Exacte codeflow van Freshness.
Dit document beschrijft Freshness zoals het nu in de code staat, niet zoals we het marketingmatig zouden samenvatten.
We lopen letterlijk langs AppModel, AppleHealthService, FreshnessCalculator,
FreshnessSnapshotStore, FreshnessUserSignalsStore,
FreshnessPromptGenerator en FreshnessTrainingAdvisor.
AppModel start de refresh, bewaart snapshots en koppelt de uitkomst aan UI en planning.
AppleHealthService bouwt ankers, selecteert HRV en splitst carry-over versus huidige dag.
FreshnessCalculator berekent baselines, componenten, confidence, trainability en day score.
FreshnessTrainingAdvisor gebruikt die ene uitkomst direct om plansessies te houden, reduceren of vervangen.
1. Scope en bronbestanden
Deze audit legt uit wat de huidige code exact doet op 13 april 2026. Het is dus geen wensdocument en ook geen vereenvoudigde supporttekst. Waar een detail belangrijk is voor de werking, staat het hier erin: opslagkeys, lookbackvensters, ankerregels, selectievolgorde, drempels, caps en latere trainingsimpact.
WattFlow/AppModel.swift
WattFlow/Services/AppleHealthService.swift
WattFlow/Services/FreshnessCalculator.swift
WattFlow/Services/FreshnessTrainingAdvisor.swift
WattFlow/Services/FreshnessUserSignalsStore.swift
WattFlow/Services/FreshnessSnapshotStore.swift
WattFlow/Domain/FreshnessModels.swift
De engine ondersteunt nog steeds dagelijkse check-ins en feedback, maar de huidige viewlaag laat vooral score- en signaalkaarten zien. In deze audit volgen we daarom de code als bron van waarheid, ook waar de zichtbare UI compacter is dan oudere documentatie suggereerde.
2. Orchestratie in de app
Alle echte Freshness-berekening loopt via AppModel.refreshFreshnessIfAvailable(...). Daar worden de
randvoorwaarden gecontroleerd, Apple Health-inputs opgehaald, subjectieve signalen geladen, de calculator gestart
en de uitkomst opgeslagen voor de rest van de app.
2.1 Refreshvoorwaarden
| Stap | Exact gedrag | Codepad |
|---|---|---|
| Sync uit | latestFreshnessComputation, freshnessState en freshnessDetailSnapshot worden leeg gemaakt en FreshnessSnapshotStore.clear() draait. |
AppModel.refreshFreshnessIfAvailable |
| Geen Health-autorisatie | Zelfde clearpad als hierboven. Er blijft dus geen verouderde Freshness zichtbaar als Health niet echt aan staat. | AppModel.refreshFreshnessIfAvailable |
| Inputs laden | AppleHealthService.shared.loadFreshnessAnalysisInputs() levert de volledige analyseketen voor vandaag plus historie. |
AppModel -> AppleHealthService |
| Persoonlijke laag laden | De huidige dag-check-in, recente check-ins, recente feedback en het laatst opgeslagen personalisatieprofiel worden meegegeven aan de calculator. | FreshnessUserSignalsStore |
| Output opslaan | Bij succes worden dailyState, detailSnapshot en het nieuwe personalisatieprofiel meteen opgeslagen. |
FreshnessSnapshotStore + FreshnessUserSignalsStore |
let inputs = try await AppleHealthService.shared.loadFreshnessAnalysisInputs()
let currentCheckIn = inputs.first.map { FreshnessUserSignalsStore.loadCheckIn(for: $0.analysisDate) }
let recentCheckIns = FreshnessUserSignalsStore.recentCheckIns()
let recentFeedback = FreshnessUserSignalsStore.recentFeedback()
let personalizationProfile = FreshnessUserSignalsStore.loadPersonalizationProfile()
let computed = FreshnessCalculator.compute(
inputs: inputs,
previousState: previousState,
subjectiveCheckIn: currentCheckIn ?? nil,
recentCheckIns: recentCheckIns,
recentFeedback: recentFeedback,
personalizationSeed: personalizationProfile
)
2.2 Snapshots en opslag
FreshnessSnapshotStore bewaart de modelversie en drie snapshots:
freshness.daily-state, freshness.detail-snapshot en
freshness.widget-snapshot. De huidige modelversie is 4.
Bij opslaan wordt ook een widgetsnapshot gemaakt, complication-data geüpdatet, phone connectivity gesynct en worden widget timelines gerefresht. Freshness is dus niet alleen een schermscore, maar ook een gedeelde outputlaag.
2.3 Subjectieve laag
De subjectieve laag bestaat uit twee losse sporen: een check-in voor vandaag en feedback achteraf. Beide leven in
FreshnessUserSignalsStore in de gedeelde suite group.app.ergtrainer.shared.
| Onderdeel | Opslagkey | Exact gedrag |
|---|---|---|
| Dag-check-in | freshness.subjective-checkins |
Wordt per dag op startOfDay gezet en overschrijft dezelfde dag. Er kunnen dus niet meerdere check-ins voor één analyse-dag opstapelen. |
| Scorefeedback | freshness.feedback-entries |
Te laag, Klopt en Te hoog worden per dag gemerged met optionele training feel. |
| Personalisatieprofiel | freshness.personalization-profile |
Wordt na elke succesvolle berekening opnieuw opgeslagen en valt terug op .neutral als er nog niets bestaat. |
saveFreshnessCheckIn(...) of recordFreshnessFeedback(...) rechtstreeks aanroept.
De engine en opslaglaag ondersteunen ze wel volledig.
3. Inputlaag uit Apple Health
AppleHealthService.loadFreshnessAnalysisInputs(dayCount: 35) bouwt standaard 35 analysedagen op, maar
kijkt daar ruimer voor terug zodat baselines en anchorselectie voldoende geschiedenis hebben.
3.1 Queries en lookback
De lookback is max(21, dayCount + 21). Met de standaard dayCount = 35 betekent dat
een queryvenster van 56 dagen terug vanaf de start van vandaag.
HRV SDNN, rusthartslag, ademhaling, Apple sleeping wrist temperature, slaapcategorieën en workouts worden parallel uit HealthKit geladen. Daarna begint pas de Freshness-specifieke selectie.
3.2 Slaapepisodes en ankers
Slaap is het anker van de hele engine. Health-samples worden eerst samengevoegd tot slaapepisodes, daarna kiest de code per analysedag precies één anker. Dat anker bepaalt analyse-datum, recovery-window, HRV-context en de scheiding tussen gisteren en vandaag.
| Stap | Exacte regel | Code |
|---|---|---|
| Slaapepisodes mergen | Alle relevante slaap- en awake-samples worden op episode-niveau samengevoegd met een merge gap van 60 minuten. | mergedSleepEpisodes(from:) |
| Asleep-intervals | Binnen een episode worden alleen asleep-intervals voor de echte slaapduur gebruikt, met een merge gap van 5 minuten. | sleepEpisode(from:) |
| Main sleep kandidaat | Minimaal 3 uur asleep en eindtijd tussen 03:00 en 12:00. Dan krijgt het anker kwaliteit .anchoredSleep. |
selectAnchor(before:) |
| Fallback sleep | Als geen main-sleep kandidaat bestaat maar wel een block van minimaal 2 uur, dan kwaliteit .fallbackSleep. |
selectAnchor(before:) |
| Inferred morning | Als er geen bruikbaar slaapblock is, gebruikt de code 07:00 op dezelfde of vorige dag als synthetisch anker. | inferredMorningAnchor(before:) |
if let mainSleep = eligibleEpisodes.first(where: {
$0.asleepDurationSeconds >= 3 * 60 * 60
&& anchorHour(for: $0.interval.end, calendar: calendar).map { 3...12 ~= $0 } == true
}) {
quality = .anchoredSleep
} else if let fallbackSleep = eligibleEpisodes.first(where: { $0.asleepDurationSeconds >= 2 * 60 * 60 }) {
quality = .fallbackSleep
} else {
quality = .inferredMorning
}
3.3 Per-dag intervallen
analysisDate = startOfDay(anchor.anchorDate)
previousAnchor.anchorDate tot anchorSleep.startDate of, zonder slaap, tot anchor.anchorDate.
anchor.anchorDate tot de volgende slaapstart of de refresh-tijd.
Van slaapstart of 3 uur voor het anker, tot maximaal 4 uur na het anker.
De activity periods bevatten niet alleen Health-energie en stappen, maar ook alle overlappende workouts. In
FreshnessActivityPeriod worden die workouts eerst genormaliseerd zodat dubbels of zwaar overlappende
sessies van hetzelfde type samenvallen in één cluster. Daarna gebruikt de code effectiveActiveEnergy als
max(activeEnergy, workoutEnergyFloorKilocalories). Daardoor kan een workout nooit “verdwijnen” als Health
toevallig minder actieve energie teruggeeft dan de workout zelf al bevat.
3.4 HRV filtering en selectie
De code gebruikt Apple Health SDNN, maar maakt daar in de calculator een log-getransformeerde readiness-proxy van. Niet elke HRV-sample mag mee. De selectie is bewust streng om workoutvervuiling uit de ochtendcontext te houden.
| Regel | Exact gedrag |
|---|---|
| Validatievenster | hrvValidationInterval start 90 minuten voor de recovery-intervalstart en eindigt op de recovery-intervaleindtijd. |
| Workoutfilter | Een HRV-sample is ongeldig als hij tijdens een workout valt of binnen 90 minuten na het workout-einde start. |
| Categorieën | Geldige samples worden opgesplitst in sleep, morning en calm. |
| Selectievolgorde | sleep (niet low) -> calm (niet low) -> morning -> sleep -> calm -> any valid -> none. |
| Kwaliteit | 0 samples = none, 1 = low, 2 = medium, 3 of meer = high. |
3.5 Overige signalen
| Signaal | Exact extractiegebied | Aggregatie |
|---|---|---|
| Rusthartslag | Binnen de recovery interval | Mediaan |
| Ademhaling tijdens slaap | Alleen over de anchor sleep intervals | Mediaan |
| Sleeping wrist temperature | Alleen over de anchor sleep intervals | Mediaan |
| Dutjes | Tussen anker en current day end | Totaal minuten, maar alleen als elk interval 15-120 min is en start tussen 09:00 en 19:00 |
4. Score-engine in FreshnessCalculator
FreshnessCalculator.compute(...) werkt in vaste lagen: historie afleiden, baselines bouwen,
statuses scoren, componenten combineren, confidence en illness toepassen, morning readiness locken en daarna
eventueel intraday begrensd bijsturen.
4.1 Historievensters en baselines
| Historie | Aantal dagen | Gebruik |
|---|---|---|
| HRV | 28 | HRV-baseline, log-proxy, rolling mean/std dev, CV |
| Rusthartslag | 28 | Persoonlijke baseline |
| Slaapduur | 14 | Slaapbaseline |
| Ademhaling | 14 | Nachtbaseline |
| Wrist temperature | 14 | Temperatuurbaseline, maar pas bruikbaar vanaf 5 nachten |
| Actieve energie | 14 | Belastingsbaseline |
| Stappen | 14 | Belastingsbaseline |
| Fragmentatie | 14 | Slaapkwaliteit |
| Slaapmidpoint | 14 | Slaapconsistentie |
| Diepe slaap | 14 | Slaapcomponent |
| Dutjes | 14 | Historiedekking en detailuitleg |
| Carry-over load units | 21 | Acute load, chronic load en ACWR |
4.2 Sleep component
De sleep component bestaat uit vier deelscores en wordt daarna gewogen opgeteld. De gewichten zijn hard in code:
duur 0.45, fragmentatie 0.25, consistentie 0.20 en diepe slaap 0.10.
| Subscore | Thresholds | Score |
|---|---|---|
| Duur | Ratio vs baseline: >= 0.96, >= 0.86, >= 0.76, lager |
95 / 80 / 62 / 42 |
| Fragmentatie | Ratio lager is beter: <= 1.05, <= 1.25, <= 1.50, hoger |
92 / 76 / 60 / 42 |
| Slaapmidpoint | Afwijking: < 30, < 60, < 90, < 120, hoger |
92 / 80 / 66 / 54 / 40 |
| Diepe slaap | Ratio vs baseline: >= 0.90, >= 0.75, >= 0.60, lager |
95 / 80 / 62 / 42 |
Daarna krijgt de hele sleep component status normal bij >= 80,
deviated bij >= 65 en anders strongDeviation.
4.3 Recovery component
Recovery is een combinatie van HRV-score, rusthartslagscore en HRV-stabiliteit. De HRV-score gebruikt niet de ruwe milliseconden, maar een log-proxy op de gekozen representatieve HRV. De stabiliteit kijkt naar de coefficient of variation van de geselecteerde HRV-samples.
| Onderdeel | Exacte thresholds | Score |
|---|---|---|
| HRV z-score | >= 0, -0.5..<0, -1.0..<-0.5, -1.5..<-1.0, lager |
96 / 84 / 68 / 54 / 40 |
| Rusthartslag ratio | <= 1.0, <= 1.03, <= 1.08, hoger |
94 / 84 / 64 / 40 |
| HRV stabiliteit | Geen kwaliteit = 65, geen current CV = 64/72, geen baseline CV = 84 of 68, anders ratio <= 1.10, <= 1.35, hoger |
65 / 64-72 / 84-68 / 88 / 74 / 56 |
De onderlinge gewichten worden niet hard als vaste percentages gebruikt, maar afgeleid uit het personalisatieprofiel:
HRV-gewicht wordt geclamped naar 0.18...0.38, rusthartslag naar 0.14...0.30 en stabiliteit
staat vast op 0.16. Daarna wordt genormaliseerd.
4.4 Load context
Load is in Freshness niet simpelweg “hoeveel heb je bewogen”, maar een combinatie van duidelijke workoutbelasting, actieve energie, stappen en een straf voor late trainingen.
loadUnits =
(clearWorkoutMinutes / 8.0)
+ (clearWorkoutEnergy / 70.0)
+ movementSupport
+ lateWorkoutBonus
| Onderdeel | Exacte regel |
|---|---|
| Duidelijke workout | Een workout telt als clear load bij minstens 30 minuten of minstens 250 kcal. |
| Movement support met workouts | min(6, max(0, (energyRatio - 1) * 4) + max(0, (stepsRatio - 1) * 2)) |
| Movement support zonder workouts | 4 punten bij ratio 2.7 / 2.4, 2 punten bij 2.2 / 1.9, anders 0 |
| Late training | Elke clear workout die eindigt op of na 20:00 zet lateClearWorkoutCount omhoog. |
| Late training straf | 6 + lateTrainingSensitivity * 8, afgerond, alleen als er een late trainingflag is. |
| Load state | spikeRisk bij ACWR >= 1.35 en carry-over >= 12; elevated bij ACWR >= 1.10 of carry-over >= 10; underloaded bij chronic load < 4 en carry-over < 4; anders optimal. |
Het uiteindelijke loadScore start op 100 en gaat naar beneden via carry-over load
(0 / -10 / -22), load state (-2 / 0 / -6 / -12) en eventueel late training.
Het resultaat wordt geclamped naar 35...100.
4.5 Caution, illness en confidence
| Laag | Exacte regel |
|---|---|
| Caution flags | Afwijkende ademhaling, afwijkende temperatuur, sterk verhoogde rusthartslag, of subjectieve check-in met averageLoad >= 4 of sleepQuality <= 2. |
| Caution level | high bij sterke ademhaling + temperatuur of 3+ flags; moderate bij 2 flags; mild bij 1 flag. |
| Caution penalty | 0 / 4 / 10 / 18 voor none / mild / moderate / high. |
| Illness status | none voor none/mild, possible voor moderate, likely voor high. |
| Illness cap | possible cap op 60, likely cap op 35. |
| Confidence penalties | HRV 0.18, RHR 0.15, slaap 0.15, nacht-signalen 0.08, korte historie 0.15, geen check-in 0.05, geen dutje-signaal 0.02, conflicten tot 0.12. |
| Confidence levels | high bij >= 0.82, moderate bij >= 0.62, daaronder low. |
De confidence wordt niet alleen als label getoond, maar trekt de score echt terug richting de neutrale waarde
82 via confidenceAdjustedScore.
4.6 Subjective laag, trends en personalisatie
De check-in wordt vertaald naar een subjectieve score via vermoeidheid, spierpijn, stress en slaapkwaliteit.
De gewichten zijn hard: fatigue 0.32, soreness 0.22, stress 0.20,
sleep quality 0.26. Het resultaat wordt geclamped naar 25...95.
De modifier is (subjectiveScore - 75) * clamp(profile.subjectiveWeight * profile.subjectiveReliability, 0.04...0.12),
afgerond naar een integer. De subjectieve laag is dus bewust begrensd en kan niet de hele score kapen.
HRV improving geeft +2, slaap improving +1, load-recovery declining -3,
fatigue declining -2 en fatigue improving +1.
let tooHighCount = feedback.filter { $0.scoreFeedback == .higherThanExpected }.count
let tooLowCount = feedback.filter { $0.scoreFeedback == .lowerThanExpected }.count
let scoreBias = clampInt(tooLowCount - tooHighCount, lower: -6, upper: 6)
let lateTrainingSensitivity = lateTrainingSensitivity(today: today, history: history)
let recoveryHalfLifeDays = clamp(
1.0 + lateTrainingSensitivity * 0.8 + max(0, hrvNoise - 0.12) * 2.5,
lower: 0.8,
upper: 2.5
)
Het neutral profiel start op: HRV 0.34, rusthartslag 0.22, slaap 0.28,
subjective 0.08, load 0.18, caution 0.18, met een neutrale
recoveryHalfLifeDays van 1.2 en lateTrainingSensitivity = 0.0.
lateTrainingSensitivity zelf wordt bepaald uit de laatste 10 analysedagen. Alleen dagen met late clear workouts
tellen mee. Vervolgens kijkt de code hoeveel van die late dagen minder dan 6,75 uur slaap hadden en hoeveel een
representatieve HRV onder 55 ms lieten zien. De ratio van die twee tellers bepaalt de sensitiviteit, geclamped naar 0.0...0.7.
5. Morning readiness en intraday
Hier zit de kern van het model. De score begint niet als een dagscore die continu zweeft, maar als een morning readiness die eerst moet “locken”. Pas daarna mag Freshness overdag nog beperkt omlaag of licht omhoog door dutjes.
5.1 Raw morning readiness
weightedMorningReadiness = weightedAverage([
(sleepComponent.totalScore, componentWeights.sleep),
(recoveryComponent.totalScore, componentWeights.recovery),
(loadScore, componentWeights.load),
(subjectiveScore ?? 75, componentWeights.subjective)
])
rawMorningReadiness =
weightedMorningReadiness
- cautionPenalty
+ subjectiveModifier
+ trendAdjustment
+ profile.scoreBias
adjustedMorningReadiness =
applyIllnessCap(
confidenceAdjustedScore(rawMorningReadiness, confidence, neutralScore: 82),
illness
)
Belangrijk: voor de huidige dag kan de subjectieve component volledig uit de morning readiness-berekening vallen als er geen check-in is. In dat geval krijgt subjective gewicht 0 in de genormaliseerde componentgewichten.
5.2 Lockvoorwaarden
| Voorwaarde | Exacte regel | Reason string |
|---|---|---|
| HRV niet verwacht | Als minder dan 5 van de laatste 14 dagen HRV-samples hebben, maar ankercontext en core signals er wel zijn, mag de score meteen locken. | hrv-not-expected-core-signals-ready |
| HRV verwacht en klaar | Als er wel ochtend-HRV verwacht wordt en de selectie-kwaliteit vandaag medium of high is. | expected-hrv-ready |
| Tijdsfallback 09:00 | Vanaf 09:00 met ankercontext en core signals mag de score locken, ook als HRV nog niet ideaal is. | time-threshold-09-with-core-signals |
| Harde fallback 11:00 | Vanaf 11:00 lockt de score altijd. | hard-fallback-11 |
if expectsMorningHRV == false, hasAnchorContext, hasCoreSignals { lock }
else if expectsMorningHRV, hasAnchorContext, hrvReady { lock }
else if hour >= 9, hasAnchorContext, hasCoreSignals { lock }
else if hour >= 11 { lock }
else { keep collectingMorningSignals }
5.3 Provisional en reuse
Als dezelfde analysedag opnieuw berekend wordt en de vorige state had al een morningLockedAt,
dan wordt die oude morning readiness hergebruikt. De ochtendscore beweegt dan niet opnieuw.
Zonder lock gebruikt de code niet de volle adjusted morning readiness, maar trekt die halverwege terug naar
neutral 82 via provisionalMorningReadiness.
5.4 Intraday begrenzing
Zodra de ochtend gelockt is, kan de score nog verschuiven door huidige dagbelasting en dutjes. Maar dat mag alleen binnen strakke grenzen:
- Base intraday adjustment: low 0, medium -4, high -8.
- Extra state correctie: spike risk -2, underloaded +1.
- Dutjesbonus: 20-39 min +2, 40-59 min +3, 60-89 min +4, 90-120 min +3.
- Cap: de candidate freshness mag nooit boven de morning readiness uitkomen.
- Wijzigingslimiet: geen wijziging bij delta van 2 punten of kleiner, maximaal 2 intraday wijzigingen per dag, maximaal 10 punten per wijziging.
Daardoor wordt Freshness expres niet een springerig live dashboard. Overdag mag hij wel reageren, maar alleen als het verschil materieel genoeg is en binnen de begrenzingen blijft.
6. Outputs van de engine
De output is niet alleen een integer score. DailyState bevat ook illness, general load, confidence,
morning lock metadata, trainability en verschillende explanationvelden. Daarnaast wordt een detail snapshot gebouwd
voor de signaalkaarten en een widgetsnapshot voor homescreen en complicaties.
6.1 DailyState en snapshots
| Output | Belangrijkste velden |
|---|---|
DailyState |
freshness, illness, generalLoad, scorePhase, confidence, morningReadinessScore, morningLockedAt, cautionLevel, trainabilityScore, trainabilityLevel, subjectiveModifierApplied, napAdjustmentApplied, lastUpdated. |
FreshnessDetailSnapshot |
Baselines, ratios, penalties, trends, explanation, recent days en de uitgewerkte component-snapshots voor de detailview. |
FreshnessWidgetSnapshot |
Compacte score-output voor widget- en complicationgebruik. |
6.2 Huidige UI-oppervlakken
Daar leeft Freshness als dagcontext via FreshnessSummaryCard. Die krijgt de score, detailsnapshot
en de status of de engine nog ochtenddata verzamelt.
In MoreTabView staat Freshness als instellingenroute met Apple Health-toggle en compacte copy.
FreshnessDetailView toont nu vooral HRV, rusthartslag, slaap, belasting, illness, ademhaling en temperatuur.
6.3 Promptgenerator
Naast de deterministische advisor bouwt WattFlow ook een compacte contextprompt. Die probeert niet zelf de score opnieuw te berekenen, maar geeft een externe AI alleen de huidige herstelcontext mee.
User freshness: 64
Morning readiness: 68
Trainability: 61 (controlled)
Illness: none
General load: medium
Confidence: 0.78 (moderate)
Caution: mild
Main reason: ...
Score changed: ...
Upcoming workouts:
...
Request:
Adjust plan, keep structure, reduce overload.
7. Effect op training en planning
De Freshness-score is niet alleen informatielaag. Elke plansessie loopt langs FreshnessTrainingAdvisor.
Die kijkt niet alleen naar de ruwe score, maar ook naar illness, caution, general load, confidence en trainability.
7.1 Advisor thresholds
| Voorwaarde | Actie | Intensity | Duration |
|---|---|---|---|
illness == likely | Replace | 0.65 | 0.60 |
illness == possible of caution moderate/high + intensief blok | Replace | 0.72 | 0.70 |
generalLoad == high + intensief blok | Replace | 0.75 | 0.75 |
trainability == low + intensief blok | Replace | 0.74 | 0.72 |
trainability == low + niet-intensief | Reduce | 0.90 | 0.78 |
confidence == low + intensief blok | Reduce | 0.90 | 0.85 |
freshness < 40 + intensief | Replace | 0.72 | 0.60-0.80 |
freshness < 40 + niet-intensief | Reduce | 0.90 | 0.60-0.80 |
freshness 60-69 | Reduce | 0.95 of 0.97 | 0.92 of 0.95 |
freshness 50-59 | Reduce | 0.95 | 0.90 |
freshness 40-49 | Reduce | 0.92 | 0.85 |
trainability == controlled + intensief blok | Reduce | 0.96 | 0.94 |
Intensieve sessies worden in deze advisor gedefinieerd als categorie .vo2max, .omslagpunt
of .test. Dat is dus een inhoudelijke workout-categoriecheck, niet alleen een drempel op IF of TSS.
7.2 Reduce versus replace
scaledWorkout(...) schaalt steady- en rampblokken in duur en intensiteit. FreeRide-blokken houden
hun freeride-karakter, maar krijgen wel een kortere duur.
recoveryReplacement(...) bouwt een vervangende hersteltraining met warm-up, rustige steady load en
cooldown. Doelduur wordt begrensd tussen 20 en 45 minuten.
8. Auditnotities en grenzen
- Geen medische engine: illness is een trainingscontextlaag, geen diagnose.
- Geen Health, geen Freshness: sync uit of niet geautoriseerd ruimt de snapshots op.
- Slaap blijft de baas: zonder bruikbare slaap valt de code terug op inferred morning anchors, maar dat is expliciet een lagere kwaliteit.
- Confidence doet echt mee: het is geen cosmetisch label; de score wordt echt teruggetrokken richting 82.
- Late training telt dubbel door: niet alleen via carry-over load, maar ook via
lateTrainingSensitivityen een extra loadstraf. - Subjectieve laag is echt, maar UI-route is nu minder zichtbaar: engine, AppModel en opslagpad zijn aanwezig; de huidige viewcode toont vooral de objectieve signaalkaarten.
Deze audit volgt de actuele Freshness-implementatie in de codebasis van WattFlow op 13 april 2026. Als code en UI opnieuw verschuiven, hoort juist deze pagina als eerste mee te veranderen.