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.