Current state — 16 april 2026

WattFlow Freshness audit — systeem zoals het nu in code staat

Deze pagina is geen oude gap-audit meer. Dit is de actuele systeembeschrijving van Freshness v3 in de WattFlow iOS-codebase. De focus ligt op de echte runtime-logica: data-ingest, slaapankers, HRV-contexten, scoring, confidence, advisor-gedrag, UI-uitlegbaarheid en validatie.

Bron van waarheid: Swift-code Backward compatible model-updates HRV-bron blijft Apple Health SDNN Geen automatische model-updates uit validatie

1. Bron van waarheid

Dit document is opnieuw opgebouwd op basis van de huidige implementatie in de app. Oude audits en specs zijn gebruikt als historische context, maar niet als leidende bron. De code is leidend.

Leidend

FreshnessCalculator.swift, AppleHealthService.swift, FreshnessTrainingAdvisor.swift, FreshnessModels.swift, AppModel.swift en de Freshness-views bepalen dit systeemgedrag.

Niet leidend

Oudere auditteksten die nog risico’s of open punten beschreven, maar inmiddels door implementatie zijn ingehaald.

Updatekader

Nieuwe velden zijn achterwaarts compatibel toegevoegd via optionals en defaults. Oude snapshots en opgeslagen states blijven decodeerbaar.

2. Runtime architectuur

Freshness draait nu als een vaste keten van Health-ingest naar analyse, naar UI, naar workout-advice en daarna naar validatie. Elke stap draagt expliciete metadata mee over datakwaliteit, lock-context en aanpassingsmodus.

Laag Verantwoordelijkheid Belangrijkste output
AppleHealthService HealthKit samples, slaapblokken, workouts, naps en anchors selecteren FreshnessAnalysisInput met anchor quality, HRV-context, validation score
FreshnessCalculator Scoreberekening, confidence, trainability, morning lock, data quality mode DailyState
FreshnessTrainingAdvisor Workout-aanpassingen op basis van state, signature en runtime mode AdvisorResult met action en reason
AppModel Refresh orchestration, throttling, preferences, snapshots, prompt, validation Gepubliceerde app-state en snapshots
Freshness UI Summary card, detail pagina, banners, explainability en settings Constrained state, stale profile, adjustment mode, trace
FreshnessValidationStore + analytics Opslaan en classificeren van analyse- en workout-events Meetlaag voor tuning, niet voor auto-learning
Apple Health data
  -> anchor selection
  -> HRV context resolution
  -> DailyState scoring
  -> UI / prompt / advisor
  -> workout execution context
  -> validation event + analytics

3. Inputs en Health-selectie

Freshness gebruikt Apple Health als primaire bron. De HRV-bron is bewust niet aangepast: de app blijft werken op Apple Health SDNN, die intern via een log-transformatie als readiness-proxy wordt gebruikt.

Gebruikte inputs

  • HRV (SDNN uit Apple Health) als geselecteerde dagwaarde plus historiek.
  • RHR, slaapduur, fragmentatie, midpoint, diepe slaap, ademhaling en temperatuur.
  • Actieve energie, stappen, carry-over load en dutjes.
  • Subjectieve check-in en workout-feedback wanneer beschikbaar.

Belangrijke grenzen

  • Geen eigen HRV-bron of alternatieve metriek naast Apple Health SDNN.
  • Geen automatische correctie van ontbrekende Health-data met modelinferentie.
  • Geen validatielus die automatisch drempels of gewichten herschrijft.
Belangrijk: een ontbrekende metric wordt niet verstopt. Die slaat door naar confidence, data quality mode, UI-banner en advisor-modus.

4. Sleep anchor en morning lock

De oude absolute ochtendlock is vervangen door een anker-gedreven systeem. Eerst wordt de meest waarschijnlijke hoofdslaap gekozen, daarna bepaalt de anchor wanneer de app de ochtendscore veilig mag locken.

Anchor selectie

  • .anchoredSleep: voorkeursanker op basis van hoofdslaap.
  • .fallbackSleep: slaapblok bruikbaar, maar minder overtuigend.
  • .inferredMorning: geen bruikbare slaap; ochtendanker wordt afgeleid.
  • .uncertain: anker is te zwak en wordt als onzeker behandeld.

Adaptieve anchoring

De anchor-selector gebruikt een historisch slaapprofiel over 21 dagen. Late slapers of ploegendienst worden daardoor niet meteen afgekeurd zolang het slaapvenster past bij hun recente patroon.

typical end time historical window minimum duration filters

Anchor validation score

  • +40 als een bruikbare HRV-sample in het recovery-venster valt.
  • +30 als een RHR-meting in datzelfde venster valt.
  • +30 als de anchor quality .anchoredSleep is.

Effect van zwakke validatie

Bij anchorValidationScore < 40 krijgt confidence een extra penalty van 0.08 en degradeert de effectieve anchor quality naar .uncertain.

Anchor quality Core window Fallback window Morning lock reasons
.anchoredSleep 120 min na anchor-end 240 min na anchor-end expected-hrv-ready, anchor-relative-core-signals, anchor-relative-fallback
.fallbackSleep 150 min 300 min Zelfde patroon, maar voorzichtiger timing
.inferredMorning / .uncertain 180 min 360 min Altijd meer terughoudend en vaak gekoppeld aan .autoReduceOnly of .informOnly
Geen anchor Absolute fallback 09:00 / 11:00 rules time-threshold-09-with-core-signals, hard-fallback-11
Operational detail: als er wel een anchor is maar het recovery-venster nog niet is afgelopen, blijft de score in collectingMorningSignals met reason waiting-for-anchor-end.

5. HRV-context en 21-daagse baselines

De engine splitst HRV nu expliciet op in contextfamilies. Daardoor wordt een slaap/ochtend-HRV niet meer zonder meer vergeleken met een rustige dagmeting als daar voldoende context-specifieke historiek voor beschikbaar is.

HRV-contexten

  • .nightRecovery voor slaap- of ochtendgerelateerde herstelmetingen.
  • .calm voor rustige, niet-slaapgebonden metingen.
  • .unavailable als er geen betrouwbare context is.

Context-match

hrvContextMatch wordt opgeslagen in state en detail snapshot. Als dagwaarde en baseline uit verschillende contextfamilies komen, krijgt confidence een extra penalty van 0.06.

Baseline Venster Opmerking
HRV 21 dagen Voorkeur voor dezelfde HRV-contextfamilie, anders expliciete mismatch
RHR 21 dagen Geharmoniseerd naar hetzelfde venster als HRV
Slaapduur, fragmentatie, midpoint, diepe slaap 21 dagen Geen 14-daagse uitzonderingen meer
Ademhaling, temperatuur, energie, stappen, dutjes 21 dagen Zelfde baseline-horizon voor coherentie
Carry-over load 21 dagen Ongewijzigd gebleven
Veiligheidsklep: baseline-harmonisatie heeft de baselineCoherence-safety valve niet verwijderd. Als recente en langere patronen uiteenlopen, zakt confidence mee.

6. Scoring, confidence en constrained mode

Freshness gebruikt nog steeds een neutrale trekwaarde van 82, maar die is nu duidelijker ingekaderd. Zodra confidence laag is en de confidence-adjusted score wordt toegepast, markeert de engine de dag expliciet als constrained in plaats van die uitkomst als een normale groene herstelscore te presenteren.

Neutraal anker

neutralFreshness = 82. Lage confidence trekt de score gecontroleerd richting neutraal, niet richting “goed herstel”.

Confidence status

high >= 0.82, moderate >= 0.62, low < 0.62.

Constrained state

isConfidenceConstrained = true als de adjusted score actief is en confidence .low is.

Slaapcomponent-gewichten

Component Gewicht Opmerking
Duur 0.40 Belangrijk, maar iets minder dominant dan eerder
Fragmentatie 0.22 Relevant, maar minder cliff-achtig meegewogen
Consistentie 0.20 Midpoint/ritme blijft wezenlijk
Diepe slaap 0.18 Bewust zwaarder dan voorheen

Confidence-penalties die nu expliciet meespelen

  • Ontbrekende HRV, RHR, slaap of herstel-ondersteunende metrics.
  • Onvoldoende historie voor stabiele baseline.
  • Ontbrekende subjectieve check-in.
  • Conflicterende signalen tussen herstelcomponenten.
  • hrvContextMatch == false geeft extra penalty van 0.06.
  • anchorValidationScore < 40 geeft extra penalty van 0.08.
  • Bij zwakke anchor-validatie degradeert de effectieve anchor quality naar .uncertain.
  • Nap-coverage telt licht mee in confidence, maar bepaalt de score niet hard.

Data quality modes

Mode Wanneer Betekenis in product
.reliable Goede confidence, degelijke anchor, normale lock-context Normale scoreweergave en volwaardig advisor-gedrag
.cautious Matige confidence of voorzichtige fallback-context Waarschuwt expliciet en duwt sneller richting .autoReduceOnly
.uncertain collectingMorningSignals, lage confidence, fallback lock, inferred/uncertain anchor Constrained UI, geen groen vertrouwen en vaak .informOnly

Intraday correcties

Dutjesbonus

  • 20–39 min: +2
  • 40–59 min: +3
  • 60–89 min: +4
  • 90–120 min: +1
  • > 120 min: +0

Begrenzing van dutjes

  • Bij ochtendscore < 55 worden alle dutjesbonussen gehalveerd met integer floor.
  • Bij illness == .possible of .likely is de dutjesbonus altijd 0.
  • Intraday verbeteringen mogen de score bijwerken, maar doen dat binnen dezelfde quality- en lock-context.
UI-contract: constrained dagen tonen een prominente banner “Onvoldoende data — herstelschatting”, onderdrukken trendlabels en forceren neutrale of gele kleurcodering in plaats van groen.

7. Automatic training adjustment modes

Trainingsaanpassingen zijn nu niet meer één impliciet gedrag. De app kent drie expliciete runtime-modi, die zowel uit gebruikersvoorkeur als uit datakwaliteit kunnen voortkomen.

Mode Betekenis Runtime-trigger
.autoAdjust Reduce én replace mogen automatisch worden toegepast Goede datakwaliteit, betrouwbare lock, geen extra blokkades
.informOnly Advisor geeft advies, maar wijzigt geen training Gebruikersvoorkeur of scorefase collectingMorningSignals
.autoReduceOnly Reduce mag, replace is geblokkeerd Lage confidence, onzekere data, fallback lock of zwakke anchor

Persistente voorkeur

De gebruiker kan een voorkeursmodus kiezen. Deze wordt opgeslagen onder freshness.adjustment-mode in FreshnessUserSignalsStore en bij elke refresh opnieuw toegepast.

Advisor logging

In .informOnly worden acties expliciet gelogd als .informedReduce of .informedReplace, zodat validatie onderscheid kan maken tussen advies en echte planwijziging.

8. Advisor-logica en workout signatures

De advisor werkt niet meer alleen met grove workout-categorieën. Elke training krijgt een signature en risicolaag, waardoor zware bloktrainingen, benchmarks en steady-hard sessies anders behandeld kunnen worden.

Signature tiers

.endurance, .steadyHard, .highAerobic, .highNeuromuscular, .benchmark.

Wat de advisor meeweegt

Freshness-score, trainability, caution, illness, carry-over load, data quality mode, runtime adjustment mode en workout-signature.

Belangrijk guardrail

Onzekere dagen verschuiven sneller naar .informOnly of .autoReduceOnly, waardoor automatische replacement agressief wordt afgeremd.

if adjustmentMode == .informOnly:
    return .informed(action: suggestedAction)

if lowConfidence or uncertainAnchor:
    prefer reduce over replace

if likelyIllness or very high risk signature:
    suppress harder

9. Personalisatie, check-ins en profiel-decay

De subjectieve laag leeft nu echt in het product, maar wordt ook actief terug naar neutraal geduwd als die te lang niet meer gevoed wordt. Dat voorkomt schijnpersonalisatie.

Invoer

  • Ochtend check-in voedt het profiel.
  • Post-workout feedback legt vast of Freshness te hoog, te laag of correct zat.
  • lastFedAt houdt het laatste subjectieve event bij.

Decay-regels

  • 0–14 dagen: profiel blijft intact.
  • 15–30 dagen: subjective weight en reliability bewegen 50% terug richting neutraal.
  • > 30 dagen: profiel valt volledig terug naar neutral.
State Betekenis UI-effect
.fresh Profiel recent gevoed Geen extra banner
.aging Profiel veroudert, maar telt nog mee Geen harde waarschuwing, wel minder subjectieve kracht
.stale Profiel is inhoudelijk terug naar neutraal gedecayed Banner: “Vul een check-in in om je profiel te vernieuwen.”

10. UI en explainability

De UI is nu afgestemd op datakwaliteit en runtime-beperkingen. De app laat niet alleen een score zien, maar ook waarom die score voorzichtig, gelockt, of nog voorlopig is.

Summary card

  • Opent als echte detailpagina, niet als slide-up.
  • Toont constrained banner op hoofdniveau, niet alleen in detail.
  • Laat progress state zien wanneer morning signals nog binnenkomen.
  • Past kleurcodering aan op basis van datakwaliteit.

Detail view

  • Toont adjustment mode, anchor quality, lock reason en validation hints.
  • Gebruikt echte secties en vaste kaartopbouw met grafiekzone boven en tekstzone onder.
  • Onderdrukt trendlabels wanneer de score constrained is.
  • Laat nieuwe dag direct zien, ook voordat alle data binnen is.
Design-principe: onzekere data wordt niet cosmetisch gladgetrokken. Als de engine voorzichtig is, moet de gebruiker dat ook zien.

11. AI prompt context

De Freshness prompt voor AI-output bevat nu niet alleen de score, maar ook de omstandigheden waaronder die score tot stand kwam. Daardoor ziet externe uitleg het verschil tussen “sterke ochtendscore” en “voorzichtige schatting”.

Prompt bevat nu expliciet

  • Freshness, morning readiness, trainability en illness.
  • Confidence plus confidence-level.
  • Data quality mode, morning lock reason en anchor quality.
  • Adjustment mode: auto adjust, inform only of auto reduce only.
  • Dominant reason, changed reason, upcoming workouts en events.

Bewuste beperking

De prompt maakt context rijker, maar schrijft geen modelregels terug. De AI-laag blijft uitleg en interpretatie; de Freshness-engine blijft de primaire beslislaag.

12. Validatie en analytics

Freshness heeft nu een echte meetlaag. Die bewaart analyse-context, workout-uitvoering en gebruikersfeedback zodat het team false positives en false negatives kan beoordelen. Die meetlaag mag echter niets automatisch terugschrijven naar calculator of advisor.

Wat wordt opgeslagen

  • Lock reason, anchor quality, anchor reason en anchor validation score.
  • Confidence level, data quality mode en adjustment mode.
  • Advisor action en reason, inclusief informational-only actions.
  • Workout signatures, TSS-afwijking, training feel en score-feedback.

Wat uitdrukkelijk niet gebeurt

  • Geen automatische herkalibratie van drempels of gewichten.
  • Geen automatische feedbacklus van analytics naar advisor.
  • Geen verborgen modelmutatie na nieuwe validatie-events.

Classificatiecriteria in analytics

// CLASSIFICATIECRITERIA (v3 definitief)
// aligned:                scoreFeedback == .correct && |TSS-afwijking| < 15%
// falsePositiveCandidate: score >= 65 && trainingFeel <= 2 && advisorAction == .none
// falseNegativeCandidate: score < 50 && trainingFeel >= 4
//                         && (advisorAction == .reduce || advisorAction == .replace)
// inconclusive:           alle overige gevallen
// NOTE: classificatieresultaten vloeien NIET automatisch terug in model of advisor.
//       Dit is uitsluitend een manueel tuning-instrument.
Praktische interpretatie: de validatielaag is er om het systeem te beoordelen en later gericht te tunen, niet om stiekem online learning in productie te doen.

13. Feature flags en code map

De hoofdverbeteringen uit het audit- en spectraject zijn nu als concrete flags en codepaden in de app verankerd. In de huidige configuratie staan deze features aan.

Actieve flags

  • freshnessAdaptiveAnchoringEnabled
  • freshnessHRVContextSplitEnabled
  • freshnessSmoothScoringEnabled
  • freshnessWorkoutSignatureAdvisorEnabled
  • freshnessBaselineCoherenceEnabled
  • freshnessValidationLoggingEnabled

Belangrijkste codepaden

  • WattFlow/Services/FreshnessCalculator.swift
  • WattFlow/Services/AppleHealthService.swift
  • WattFlow/Services/FreshnessTrainingAdvisor.swift
  • WattFlow/Services/FreshnessValidationAnalytics.swift

UI/code koppeling

  • FreshnessViews.swift voor summary card en banners
  • FreshnessDetailView.swift voor detailtrace
  • MoreTabView.swift voor validatie-ingang
  • AppModel.swift voor refresh, prompt en snapshot-flow

14. Grenzen en bewuste keuzes

Het systeem is nadrukkelijk veiliger en beter uitlegbaar geworden, maar behoudt ook een paar bewuste productkeuzes. Die zijn geen vergeten gaten; ze zijn expliciet zo gelaten.

Bewust behouden

  • Neutrale scoretrek naar 82 blijft bestaan, maar wordt zichtbaar gemaakt via constrained mode.
  • SDNN uit Apple Health blijft de HRV-input, zonder eigen HRV-stack.
  • Validation analytics beïnvloeden het model niet automatisch.
  • Fallback locks bestaan nog, maar pas nadat anchor-relatieve paden zijn geprobeerd.

Reële operationele grenzen

  • Data-arm gebruikers zullen vaker in cautious of uncertain mode landen.
  • Een inferred of uncertain anchor blijft een degradatiestand, geen verborgen “bijna net zo goed”.
  • Subjectieve personalisatie zonder recente check-ins vervalt bewust terug naar neutraal.
  • De validatielaag helpt pas na echte gebruiksdata; hij lost niets instantaan op.