Loadbudget

PLAN

How does the app plan the days ahead of the athlete?

Binds: code, architecture

No screen draws the load ahead. The chart “Running each week” on “Your running” draws the running target of each week up to race day. This page states the shape of a screen of the load ahead, the way ios-app/docs/architecture/PERSISTENCE.md states a sync shape with no commit behind it.

The taper curve is built, and the room for today reads it. LOAD.md names the day budget, and Taper.swift in Core holds the curve. So the section “The taper is the one prescribed target” states what runs. The paragraphs on her running state what runs too. The last sections state what runs as well. The rest of this page states what nobody built.

What this page does not decide

Race.swift in Core already states what a race is to the model: a name, a day, and a shape. Today that shape carries one case, fixedLength, with a RaceLength and an optional cutoff. No field holds a target time. Garmin, Runna and TrainingPeaks demand none either. Some race formats carry no distance up front: a backyard ultra, a timed race, and a mixed-format race such as HYROX. Each one needs a new RaceShape case, and not a change to RaceLength itself.

Both sports spend one ledger, in one unit. This page adds no second ledger and no second unit. A gym session and a run still spend the same number. This page decides no part of the rule that reads the taper target.

This page decides no session. It decides the target across the weeks ahead, and the shape that a future screen will draw for them.

The plan will store nothing

The plan will not be a shape that a write computes once. Every read will build it fresh, from Race.day and from the baseline that usualWeek(from:at:in:) already returns in WeekSpend.swift in Core. The weekly running target reads her runs of each week, and it stores nothing either.

So a missed session will force no redesign of the plan. A missed session, an illness, or a rest week will change the sessions that usualWeek reads. Nothing else will change. The next read will carry that change forward, because the read will compute from the sessions again.

No stored plan will sit behind it. No stored plan will drift from what she did. No separate degradation rule will exist, because no state will exist for reality to diverge from.

Before the taper, the target is flat

docs/GLOSSARY.md names the build phase. This page redefines no word from the glossary. It names no phase.

For every week before the taper, the target is her current baseline, unchanged. It does not rise. What she actually trains, in the build phase that the glossary names, still can. A future screen will draw that target as a flat line.

No published rate belongs in that line. Neither the acute-to-chronic workload ratio nor the 10-percent-per-week rule carries evidence that survives a trial. A rising target is a rate stated in advance. Every rate that this literature tested failed.

The app already refuses a safe band and a threshold on the week that passed. A rising target makes that same claim about a week that comes later. This page holds that refusal forward.

The flat line will still be a shape she can judge. If nothing changes, it will state what continues, so she can compare her plan against it. It will state no target for her to hit.

Her running is the one exception, and it still states no rate in advance. The weekly running target in runningTargets(of:toward:at:in:) is a ceiling at a multiple of her running fitness. So the minutes that she ran earn the rise, and no week ahead states it. Next week holds the target of this week. Every week after it holds that target until the taper. Through the taper it falls by the taper scale that the room for today reads.

The refusal above still covers every other rate.

The taper is the one prescribed target

Confirmed, with the number. Bosquet 2007 and a 2023 replication in PLoS One put the optimum taper at two weeks. They also support the exponential fall that the formula below follows.

docs/product/JOURNEY.md, phase 05, names the window: weeks 13 and 14, the 14 days that end on Race.day itself. This page follows that window, and not one of its own.

The target on day n before the race (n from 13 down to 0) is:

target(n) = baseline × exp(k × (n − 13))

baseline is her usualWeek value on the day the taper starts. At n = 13, the exponent is zero, a property of the formula and not a claim about the app. So target(13) equals baseline, and the line stays continuous with the flat line before it.

k is log 2 divided by 13. So target(0) is half of the baseline. That is a cut of 50 percent, inside the cut of 41 to 60 percent that the taper meta-analysis of Bosquet and colleagues found. taperDecay in Taper.swift holds the expression. A decimal there hides that claim. The whole window, from target(13) to target(0), sits inside the two-week taper that the same meta-analysis tests. No day here extrapolates past the evidence.

The baseline is read on the day the taper starts, and not today. Inside the window older pre-taper weeks can leave the 28-day baseline window. A target built from the baseline at this moment then shifts with that window.

Nothing is stored to hold that off. usualWeek skips a day that comes after the moment it reads. So the baseline at a past moment is a function of the sessions before it alone.

Nothing reads target(0). The app will suggest nothing on Race.day itself. The formula still defines a value there, because a formula with a hole in it costs more to build. No read of the plan reaches that value.

The window that a read admits runs from day 13 down to day 1. On race day itself the plan states no target. The baseline is then the target again, and that day reads the way any day outside a taper reads.

The target limits the room for a day. It leaves the ledger untouched. A target is not a day budget: the app reads the first, and she reads the second.

Only her running takes an instruction from it. The app suggests a run and no lift, so a falling target reaches her running alone. Her lifting still spends, because both sports spend one ledger. Whatever she logs at the gym lands in that ledger unshaped, the same as every other week.

A race with no single length still carries a day. This formula reads only Race.day and usualWeek. One RaceLength cannot state the whole of the future race formats above. Each one still tapers toward its date under this same rule.

A target needs a baseline first. usualWeek returns .arrives(inDays:) before personalBaselineDays of history exist. The app then states how many days the baseline still needs, the way the ledger already does. The run of the day then holds no cap from the week of both sports. A future plan screen will draw no flat line and no taper curve.

The taper never touches her lifting

What her lifting does in the two weeks of the taper is her call.

This page decides no session. It adds no suggestion of its own for her lifting.

This is what keeps the taper from costing her twice. An athlete who cuts both sports at once pays for the taper twice. This plan cuts only one sport. Whatever she lifts in the last two weeks reaches the ledger through the same read as any other session. The taper target above never sees it.

The running shape does not transfer to the gym on its own. No study tapers strength work into an endurance race by cutting its volume and holding its intensity. This page decides no such shape. The app will never issue that instruction.

Predicting a lifting day from her weekday history

The app predicts a lifting day, and she confirms it with one tap.

The app reads the twelve whole weeks before today, and liftingWeeks holds that count. Today starts at calendar.startOfDay(for: now). Week 1 is the seven days that end at the start of today. Week 2 is the seven days before week 1. The count ends at week 12.

When a week starts on or after the day of her oldest session, the app reads it. So weeksRead is a number from 0 to liftingWeeks.

With fewer than liftingHistoryFloor weeks read, the answer states not enough history. That answer carries weeksRead alone.

Otherwise the app counts the read weeks in which a strength session starts on the same weekday as the day in question. SessionActivity.strength names that activity. The weekday comes from calendar.component(.weekday, from:).

When 2 * weeksWithLift >= weeksRead, the day is a likely lifting day. Otherwise the day is unlikely.

The answer carries the weekday, weeksWithLift and weeksRead. The ledger screen states that Thursday is usually a lifting day, and it prints the count behind it.

The answer also carries latestLiftTime: the latest start of a lift on that weekday in the read weeks, as a time of day. When today is a likely lifting day and the time of day is liftingGrace past latestLiftTime, no lift is ahead today. The lift sits ahead tomorrow instead. liftingGrace keeps a lift a little later than usual inside the plan. Without it, a lift at the usual time leaves the plan a few minutes after its start.

She answers for the lift that is still ahead

LiftingDayView states her saved answer or the current assumption of a lift ahead. For a lift ahead, SuggestionReason.liftAheadBasis identifies the source. SuggestionReason.liftedToday states whether the sessions hold a completed lift today. After that lift arrives, the summary states that she already lifted today. The lifting edit action and question disappear. Before that lift arrives, its edit action opens the lifting answers. After a successful save, the editor closes. After a failed save, the editor stays open. Its summary keeps the previous answer or assumption. GymSessionNotice in LoadbudgetDayRecordStore holds the stored answers. The editor offers these answers: lift today, and skip lifting today. A stored answer of tomorrow still reads as a lift ahead on the next day. Earlier heavy or light answers select the corresponding day. A new answer asks for no intensity or muscle group. The answer reaches the record of the day.

The question asks nothing that Apple Health already holds. A lift that she finished is a workout, and the ledger reads it. What Health cannot hold is what that lift cost her legs. ReadinessReason.heavyLegs carries that, beside the percent that she sets.

She says how ready she is

ReadinessView states her saved percent or the sleep starting value and its source. SuggestionReason.readinessBasis names her slider or the sleep start. It derives that source from Readiness.setByHer. Its edit action opens the slider. The slider keeps a draft separate from the stored summary. After a failed save, the slider keeps the draft. Its summary keeps the stored percent or starting value. The slider reads ReadinessPercent.lowest and ReadinessPercent.highest for its ends. The thumb starts at the sleep preset, unless she stored an answer today. The line under the question states the starting percent and its source. After she moves the thumb, the line states her percent alone.

When her finger leaves the thumb, the slider writes. A write on every step stores each number that the thumb passed over. A change with no finger on the thumb writes at once, and a VoiceOver adjustment is such a change.

The reason appears once a percent under ReadinessPercent.highest exists. A picker then offers each case of ReadinessReason, and none is the choice until she picks one. A reason on its own stores a percent that she never gave. A reason says why she is not at 100 percent, so a percent of 100 hides the picker.

A move back to 100 clears the reason. The write that stores 100 also stores none. A reason left on the row names a word that no control shows.

LedgerModel.answerReadiness(_:reason:) writes both onto the record of the day. Like the gym answer, it reads the row again first, and a read that fails writes nothing. runOfTheDay(_:in:) records the reason heavyLegs in its explanation. The reason applies no separate cut. The rule reads the percent through readiness(selfPercent:lastNightAsleep:usualAsleep:). No rule reads the other reasons, because no source names a cut for any one of them.

An answer of tomorrow belongs to the day that she gave it. The record of Thursday carries a lift of Friday. The record of Friday carries its own answer.

Lifting answers describe commitments and move no run. The weekday prediction moves no run either. A lift ahead tomorrow keeps sprint intervals off today, as RUN_OF_THE_DAY.md, section “Lifting facts and commitments”, states. Readiness still limits the suggestion. Completed lifts still spend from the shared weekly budget.

Previewing today’s workout

RunOfTheDayView draws the lifting and readiness rows before the workout preview. The Watch queue action follows the rows, and it shows whether the preview is open or closed. The preview opens only the steps. On a day with no run, both input editors remain available. The card draws no workout preview. Both editors share the day-record failure line. A completed workout keeps its scheduled plan and actual result.

Previewing tomorrow at an assumed readiness

LedgerModel.tomorrowPreview(atReadiness:) evaluates tomorrow’s run. It uses the existing coach rule. Its date is the next calendar day after LedgerModel.budget.now. It uses only sessions whose start is no later than LedgerModel.budget.now. When that cached day differs from the model clock, it offers no preview. When Foundation posts NSCalendarDayChanged, LedgerView rereads. It rebuilds the budget, race selection and workout steps for tomorrow. When tomorrow is the selected race day, it returns no preview. While the surface state of LedgerModel.dailyPresentation is suggestion, it returns no preview. That presentation holds no completed session. The preview returns after she completes today’s workout. When today offers no run, this condition does not apply. Today’s record supplies yesterday’s commitments for that evaluation. No stored readiness answer supplies tomorrow’s assumed percent. The method reads no sleep for tomorrow. It writes no day record. TomorrowRunPreviewView starts its local slider at ReadinessPercent.highest. Moving the slider recalculates the suggestion and its steps. The total workout time sums every step. The extra easy cooldown counts too. A new preview day resets the slider. RunOfTheDayView shares the workout summary and steps with this preview. Its tomorrow mode offers no daily input editors or Watch queue action. The actual run of the day keeps its existing readiness and sleep rules.