Date: 2026-03-13
Repository: Apple-watch
Scope: repo-wide review with emphasis on correctness, optimization, performance, abandoned code, and modernization opportunities.
swift testinapps/HeartCoach: 641 tests passed, 0 failures on 2026-03-13 (up from 461 after test restructuring).swift testno longer reproduces the earlier SwiftPM unhandled-file warning or theThumpBuddyFacemacOS availability warning on branchfix/deterministic-test-seeds.Important scope note: the default SwiftPM target still excludes dataset-validation and engine time-series suites inapps/HeartCoach/Package.swift, so the 461-test pass is not the full extended validation surface.- ✅ RESOLVED (commit 3e47b3d): Test restructuring moved EngineTimeSeries-dependent tests into ThumpTimeSeriesTests target and un-excluded EngineKPIValidationTests.
swift testnow runs both ThumpTests and ThumpTimeSeriesTests (641 total). Only iOS-only tests (needing DashboardViewModel/StressViewModel), DatasetValidation (needs external CSV data), and AlgorithmComparisonTests (pre-existing SIGSEGV) remain excluded.
- Verified current branch:
fix/deterministic-test-seeds - Verified commits:
0b080ebfix: resolve code review findings and stabilize flaky testscba5d71docs: update BUG_REGISTRY and PROJECT_DOCUMENTATION with fixesad42000fix: share LocalStore with NotificationService and pass consecutiveAlert to ReadinessEnginedcbee72feat: wire notification scheduling from live assessment pipeline (CR-001)218b79bfix: batch HealthKit queries, real zoneMinutes, perf fixes, flaky tests, orphan cleanup7fbe763fix: string interpolation compile error in DashboardViewModel, improve SWELL-HRV validation3e47b3dtest: include more test files in swift test, move EngineTimeSeries-dependent tests
- The originally enumerated code-review fixes appear landed on this branch.
- This file now treats those as resolved audit items and keeps only genuinely open product-quality and calibration work below.
- Duplicate snapshot persistence fix:
- LocalStore.swift upserts by calendar day at lines 148-164.
- Explicit nudge completion tracking fix:
- DashboardViewModel.swift records explicit completion dates at lines 236-263.
- InsightsViewModel.swift reads
nudgeCompletionDatesfor weekly completion rate at lines 173-183.
- Same-day streak inflation fix:
- DashboardViewModel.swift guards streak credit with
lastStreakCreditDateat lines 252-262.
- DashboardViewModel.swift guards streak credit with
- Readiness stress-input integration fix:
- DashboardViewModel.swift now computes and passes the real
StressEnginescore at lines 438-460.
- DashboardViewModel.swift now computes and passes the real
- SwiftPM fixture-warning cleanup:
- Package.swift excludes
Validation/DataandEngineTimeSeries/Resultsat lines 24-53.
- Package.swift excludes
ThumpBuddyFaceavailability fix:- ThumpBuddyFace.swift wraps
.symbolEffect(.bounce)in an availability check at lines 257-264.
- ThumpBuddyFace.swift wraps
HeartTrendEnginebaseline overlap fix:- HeartTrendEngine.swift excludes the current week from the baseline at lines 462-486.
CoachingEnginedate-anchor fix:- CoachingEngine.swift uses
current.dateat lines 48-52.
- CoachingEngine.swift uses
CorrelationEngineactivity-minutes fix:- CorrelationEngine.swift uses
activityMinutesat lines 91-95.
- CorrelationEngine.swift uses
SmartNudgeSchedulerdate-context fix:- SmartNudgeScheduler.swift uses
todaySnapshot?.dateat lines 240-243. - SmartNudgeScheduler.swift uses
todaySnapshot?.dateat lines 329-332.
- SmartNudgeScheduler.swift uses
- Notification pipeline fix:
- ThumpiOSApp.swift injects shared
NotificationServiceand requests authorization during startup. - DashboardView.swift binds the environment notification service into the view model.
- DashboardViewModel.swift schedules anomaly alerts and smart nudges from live assessment output at the end of
refresh().
- ThumpiOSApp.swift injects shared
These are the items I would keep in the review because they are not actually complete:
-
Startup path still needs one-shot hardening and measurement.
performStartupTasks()is still attached to the routed root view in ThumpiOSApp.swift, so route changes can still rerun startup work.- Launch still eagerly instantiates several services and synchronously hydrates
LocalStore.
-
Large-file maintainability hotspots remain.
-
WatchFeedbackBridgeis still a kept-but-unused subsystem. -
System design documentation still has drift.
- MASTER_SYSTEM_DESIGN.md remains useful for intent, but it is not a fully current implementation source of truth.
-
Stress-engine product trust is still open.
- Repo-wide stress-calibration status should now be read from the dedicated report:
-
BioAgeEngine,CorrelationEngine, andSmartNudgeSchedulerstill need stronger validation before their outputs deserve high-trust product language. -
Broader real-world validation is still uneven outside the stress work.
- Stress now has the strongest executed real-data gate in the repo.
- The other engines still rely more heavily on synthetic or heuristic validation.
These files currently have no production call sites and are increasing maintenance surface area:
— ✅ Moved toapps/HeartCoach/iOS/Services/AlertMetricsService.swift.unused/(commit218b79b)— ✅ Moved toapps/HeartCoach/iOS/Services/ConfigLoader.swift.unused/(commit218b79b)— ✅ Moved toapps/HeartCoach/File.swift.unused/(commit218b79b)apps/HeartCoach/Shared/Services/WatchFeedbackBridge.swift- Dedup/queueing bridge is tested, but not integrated into the shipping watch/iPhone feedback path. Kept — likely needed for future watch connectivity.
Overall assessment: good intent and strong test coverage, but uneven maintainability.
Strengths:
- The core engine layer is reasonably well separated from the UI layer.
- There is substantial automated coverage;
swift testnow passes 641 tests (up from 461 after test restructuring in commit3e47b3d). - Concurrency boundaries are usually explicit, especially around
@MainActorview models and WatchConnectivity callbacks. - Most files include useful doc comments, which makes onboarding easier.
Code-quality risks:
- Several files are very large and are now carrying too many responsibilities:
apps/HeartCoach/iOS/Views/DashboardView.swiftis about 2,197 lines.apps/HeartCoach/Watch/Views/WatchInsightFlowView.swiftis about 1,715 lines.apps/HeartCoach/Shared/Models/HeartModels.swiftis about 1,598 lines.apps/HeartCoach/iOS/Views/StressView.swiftis about 1,228 lines.apps/HeartCoach/iOS/Views/TrendsView.swiftis about 1,020 lines.
Dependency injection is inconsistent. Some screens use environment-scoped shared services, while others instantiate fresh service objects inside view models.- ✅ IMPROVED (commit 218b79b): InsightsViewModel, TrendsViewModel, and StressViewModel now receive the shared HealthKitService via
bind()from their views, matching the DashboardViewModel pattern. (PERF-4) - Warning debt improved on this branch: the earlier SwiftPM fixture warnings and the
ThumpBuddyFaceavailability warning are no longer reproduced in the default package test run. There is still visible architecture drift between “implemented” and “used” code, especially for the still-partialNotificationServiceintegration,AlertMetricsService,ConfigLoader, andWatchFeedbackBridge.- ✅ IMPROVED:
NotificationServicefully wired (commitdcbee72).AlertMetricsServiceandConfigLoadermoved to.unused/(commit218b79b). OnlyWatchFeedbackBridgeremains as a kept-but-unused subsystem.
Recommendations:
- Break oversized views into feature-focused subviews and small presentation models.
- Standardize on app-level dependency injection for long-lived services.
- Treat warnings as backlog items, not harmless noise.
- Remove or integrate orphaned subsystems so the codebase reflects the runtime architecture more honestly.
Note: this review did not capture a real cold-launch benchmark on device or simulator. The points below are based on static analysis of the startup path.
Launch-path observations:
- The app creates
HealthKitService,SubscriptionService,ConnectivityService, andLocalStoreeagerly at app startup inapps/HeartCoach/iOS/ThumpiOSApp.swift:29-39. - Startup work is attached to the routed root view via
.task { await performStartupTasks() }inapps/HeartCoach/iOS/ThumpiOSApp.swift:43-53. performStartupTasks()then binds connectivity, registers MetricKit, loads StoreKit products, and refreshes subscription status in sequence inapps/HeartCoach/iOS/ThumpiOSApp.swift:93-119.SubscriptionServicealready kicks offupdateSubscriptionStatus()during its own initialization inapps/HeartCoach/iOS/Services/SubscriptionService.swift:74-84, so app launch currently does overlapping subscription-status work.- ✅ RESOLVED (commit 218b79b, PERF-1): Removed redundant
updateSubscriptionStatus()fromSubscriptionService.init(). Only called once inperformStartupTasks(). ConnectivityServiceactivatesWCSessionimmediately inapps/HeartCoach/iOS/Services/ConnectivityService.swift:37-40.LocalStoresynchronously hydrates and decrypts persisted state during initialization inapps/HeartCoach/Shared/Services/LocalStore.swift:66-90.
Startup-time risks:
- Because the root
.taskis attached to the routed root view,performStartupTasks()can rerun as the app transitions between legal gate, onboarding, and main UI. That creates repeated startup churn and makes one-time initialization less predictable. ✅ RESOLVED (commit 218b79b, PERF-2): Deferred toloadProducts()is eager launch work even though product metadata is only needed when the paywall is shown.PaywallView.task{}.Subscription status is refreshed both in✅ RESOLVED (commit 218b79b, PERF-1): Removed fromSubscriptionService.init()and again inperformStartupTasks().init().MetricKitService.start()has no one-shot guard, so repeated startup-task execution can re-register the subscriber unnecessarily.- ✅ RESOLVED (commit 218b79b, PERF-5): Added
isStartedflag toMetricKitService.start()to guard against repeated registration.
Assessment:
- First paint is probably still acceptable on modern hardware because the launch work is asynchronous and not all of it blocks initial UI presentation.
- Even so, launch is doing more work than necessary, and some of it is duplicated or triggered earlier than user value requires.
Recommendations:
- Make startup initialization one-shot instead of tying it to a routed root view lifecycle.
Defer✅ RESOLVED (commit 218b79b, PERF-2):loadProducts()until the paywall is first opened, or schedule it after first interaction instead of during launch.loadProducts()deferred toPaywallView.task{}.Keep only one subscription-status refresh path.✅ RESOLVED (commit 218b79b, PERF-1): Removed redundant call fromSubscriptionService.init().- Consider lazily activating watch connectivity if the watch feature is not immediately needed.
- Add explicit startup instrumentation:
- Use
MXApplicationLaunchMetricfrom MetricKit for production trend tracking. - Add
os_signpostspans aroundperformStartupTasks(), StoreKit initialization, and LocalStore hydration. - Measure cold and warm launch in Instruments before and after cleanup work.
- Use
I reviewed apps/HeartCoach/MASTER_SYSTEM_DESIGN.md to compare the intended architecture against the current code. The document is useful for understanding product intent and the intended engine interactions, but parts of it have drifted from reality.
Useful context from the design doc:
- The intended core loop is clear: HealthKit snapshot →
HeartTrendEngine→ReadinessEngine/NudgeGenerator/BuddyRecommendationEngine→ dashboard + watch surfaces. - The doc makes the architecture goals explicit: rule-based engines, on-device processing, encrypted storage, and closed-loop coaching.
- The engine inventory and product framing are still helpful for understanding why the code is structured as multiple specialized engines instead of one central model.
Doc-to-code mismatches:
MASTER_SYSTEM_DESIGN.mdsaysBuddyRecommendationEngineis not wired toDashboardViewModel(Gap 1), but the current code does callcomputeBuddyRecommendations()inDashboardViewModel.refresh()and rendersbuddyRecommendationsSectioninDashboardView.- The same doc says there is no onboarding health disclaimer gate (
Gap 9), butOnboardingViewcontains a dedicated disclaimer page and blocks progression until the toggle is accepted. - The production checklist marks iOS/watch
Info.plistfiles andPrivacyInfo.xcprivacyas TODO, but those files exist in the repo today. Gap 7saysnudgeSectionwas fixed and wired into the dashboard, but the current dashboard comments say it was replaced bybuddyRecommendationsSection, and the oldnudgeSectionstill exists as unused code.- File inventory metadata is stale. The document’s stored line counts are lower than the current files in several major views, so it should not be used as a precise sizing/source-of-truth artifact anymore.
Recommendation:
- Treat
MASTER_SYSTEM_DESIGN.mdas architectural intent, not as exact implementation truth. - Add a short “verified against code on ” maintenance pass whenever major flows change.
- Remove or update stale gap items so the document does not actively mislead future refactors or reviews.
This section answers two questions for each engine:
- Is the current data and validation story enough?
- Is the current output quality good enough for real users?
Short version:
- Good enough for a wellness prototype:
HeartTrendEngine,BuddyRecommendationEngine,NudgeGenerator, parts ofStressEngineandReadinessEngine. - Not good enough yet for strong user trust or strong claims:
BioAgeEngine,CoachingEngine,HeartRateZoneEngine,SmartNudgeScheduler,CorrelationEngine.
Assessment:
- This is still the strongest engine in the repo. It has the clearest orchestration role, the broadest signal coverage, and the most coherent output model.
- It is probably good enough for prototype-level daily status output.
Strengths:
- Solid separation of anomaly, regression, stress-pattern, scenario, and recovery-trend logic.
- Strong synthetic/unit support in the repo.
- Output shape (
HeartAssessment) is rich enough to power multiple surfaces cleanly.
Gaps / bugs:
- The prior baseline-overlap bug appears fixed on this branch.
weekOverWeekTrend()now excludes the most recent seven snapshots before computing the baseline mean. - Week-over-week logic is RHR-only. That may be acceptable for a first pass, but it means “trend” is narrower than the UI language suggests.
- Real-world validation is still weak for this specific engine because there is no equivalent executed external-dataset gate comparable to the new stress-engine validation workflow.
Verdict:
- Enough for a prototype daily assessment engine.
- Not enough yet for claims of strong trend sensitivity or calibrated week-over-week analytics.
Assessment:
- The engine is directionally useful, especially for relative ranking and “higher vs lower stress” days.
- It is not yet convincingly calibrated enough for users to trust the absolute numeric score.
Strengths:
- Better grounded than many wellness heuristics because it explicitly uses personal baselines.
- RHR-primary weighting is a sensible correction given the design notes and TODO rationale.
- The engine is pure and easy to test.
Gaps / risks:
- The repo itself still marks this engine as calibration work in progress (
apps/HeartCoach/TODO/01-stress-engine-upgrade.md). - The stress-validation story has improved substantially since the original review:
- local SWELL, PhysioNet, and WESAD data are now present
- the dedicated validation harness has been executed against those datasets
- the detailed current status now lives in STRESS_ENGINE_VALIDATION_REPORT.md
- The remaining issue is not absence of validation anymore; it is that the current single-formula product score still does not generalize cleanly across those datasets.
- The output score is still a heuristic composite with tuned constants rather than a validated real-world scale.
- The description text can make the score feel more certain than the calibration evidence currently justifies.
Verdict:
- Enough for relative guidance in a prototype.
- Not enough for strong absolute statements like “your stress is 72/100” without more out-of-sample real-user validation.
Assessment:
- The architecture is solid and probably the cleanest “composite wellness” model in the repo.
- The engine itself is more mature than its current integration.
Strengths:
- Clear pillar model, weight re-normalization, and understandable scoring.
- Sleep and recovery pillars are intuitive and reasonably explainable.
- Good extensibility; the TODO plan is incremental rather than a rewrite.
Gaps / bugs:
- The engine still lacks the planned richer recovery inputs described in
apps/HeartCoach/TODO/04-readiness-engine-upgrade.md. - Integration improved on this branch:
computeReadiness()inDashboardViewModelnow passes the realStressEngine.compute()score instead of the coarse70.0flag path. One meaningful gap remains:DashboardViewModelstill does not passassessment?.consecutiveAlertintoReadinessEngine.compute(...), even though the engine supports that overtraining cap.- ✅ RESOLVED (commit ad42000):
DashboardViewModel.computeReadiness()now passesconsecutiveAlert: assessment?.consecutiveAlert. File:apps/HeartCoach/iOS/ViewModels/DashboardViewModel.swift:~460. - The shipped readiness result now uses the full engine contract (stress score + consecutiveAlert overtraining cap).
Verdict:
- Engine design: good enough.
- Current app output: materially improved and now uses the full currently supported dashboard input contract.
- Remaining gaps are richer recovery inputs and stronger validation, not missing wiring in the current dashboard path.
Assessment:
- This is the least trustworthy numeric output in the core wellness stack.
- It is okay as a soft motivational feature, but not strong enough for a serious “bio age” representation.
Strengths:
- Missing-data handling is decent.
- The explanation/breakdown model is better than a single opaque number.
Gaps / risks:
- BMI is approximated from weight plus sex-based average height in
apps/HeartCoach/Shared/Engine/BioAgeEngine.swift:170-185. That creates obvious bias for users who are much shorter or taller than the assumed average. - The engine is still under active rethinking per
apps/HeartCoach/TODO/02-bioage-engine-upgrade.md. - The output mixes signals with very different evidence quality into a single age number, which can look more validated than it is.
- Validation is especially thin here: NTNU reference logic is mentioned, but the repo does not contain a robust out-of-sample benchmark for the full composite.
Verdict:
- Not enough for a high-trust user-facing absolute number.
- Acceptable only if framed clearly as a lightweight wellness estimate and deprioritized from major product claims.
Assessment:
- One of the better user-facing engines in the project.
- It adds real value by turning multiple upstream signals into prioritized actions.
Strengths:
- Clear priority ordering and deduplication.
- Easy to reason about and easy to extend.
- Better product value than many of the raw numeric engines because it produces actionable output.
Gaps / risks:
- It inherits upstream weaknesses directly; if trend, stress, or readiness are off, recommendation quality will drift too.
- Some language still edges toward stronger inference than the underlying data warrants. Example: the consecutive alert message says the body may be “fighting something off,” which may feel more diagnostic than a wellness app should sound.
- There is no explicit uncertainty presentation per recommendation.
Verdict:
- Good enough for production-style UX if the upstream engines are improved.
- Already one of the strongest parts of the product.
Assessment:
- Valuable conceptually, but currently too heuristic to be treated as a dependable engine.
- It is more of a copy/projection layer than a validated analytics layer.
Strengths:
- Good motivational framing.
- Connects behavior and physiology in a way users can understand.
Gaps / bugs:
generateReport()anchors “this week” and “last week” toDate()instead ofcurrent.dateinapps/HeartCoach/Shared/Engine/CoachingEngine.swift:48-54. That makes historical replay and deterministic backtesting inaccurate whenever the evaluated snapshot is not “today.”- ✅ FIXED (2026-03-13): Replaced
Date()withcurrent.dateso weekly comparisons use the snapshot's own date context.
- ✅ FIXED (2026-03-13): Replaced
- Projection text is aspirational and research-inspired, but not individualized enough to justify precise-looking forecasts.
Zone-driven coaching is weakened further by the fact that real HealthKit snapshots currently never populate✅ RESOLVED (commit 218b79b, CR-013):zoneMinutes.queryZoneMinutes(for:)now populates real zone data from workout HR samples.
Verdict:
- Not enough for high-confidence projections.
- Even with the date-anchor bug fixed, it still needs stronger validation before its outputs should be treated as more than motivational guidance.
Assessment:
- Good library-based engine for prototype coaching.
- Output is generally usable, but quality depends heavily on upstream signal quality and can become generic.
Strengths:
- Clear priority order.
- Readiness gating is a smart product decision that prevents obviously bad recommendations on poor-recovery days.
- Multiple-nudge support is useful.
Gaps / quality risks:
- Secondary suggestions can degrade into generic fallback content like hydration reminders, which may make the engine feel less personalized on borderline-data days.
- It remains largely a curated rule library, so repetition risk grows as users spend more time in the product.
- Recommendation specificity is only as strong as the upstream engine inputs.
Verdict:
- Good enough for now.
- Needs more personalization depth and more “why this today” grounding to stay strong over time.
Assessment:
- The standalone algorithm is plausible, and the shipped product path now can feed it real data for users with tracked workouts.
- The remaining issue is output quality and validation depth, not a missing ingestion pipeline.
Strengths:
- Karvonen-based zone computation is a sensible approach.
- Weekly zone summary logic is straightforward and explainable.
Gaps / bugs:
HealthKitService.fetchSnapshot()hardcodeszoneMinutes: []inapps/HeartCoach/iOS/Services/HealthKitService.swift:231-239. ⬚ OPEN — requires HealthKit workout session ingestion to populate real zone data.- ✅ RESOLVED (commit 218b79b): Added
queryZoneMinutes(for:)method that queries workout HR samples and buckets into 5 zones based on age-estimated max HR (220-age).fetchSnapshot(for:)now uses real zone data viaasync let zones = queryZoneMinutes(for: date). DashboardViewModel.computeZoneAnalysis()then bails out unless there are 5 populated zone values inapps/HeartCoach/iOS/ViewModels/DashboardViewModel.swift:455-462.- As a result, zone analysis/coaching now works only for users whose recorded workouts yield enough usable heart-rate-zone data; it is no longer mock-only, but it is still sparse for lightly tracked users.
- There is also a smaller correctness issue:
computeZones()documents sex-aware HRmax handling, but the current implementation does not materially apply a different formula.
Verdict:
Not enough as shipped because the data pipeline into the engine is missing.- ✅ IMPROVED (commit 218b79b, CR-013): Data pipeline now exists —
queryZoneMinutes(for:)ingests real workout HR samples. Verdict: engine is now usable with real data for users who do tracked workouts.
Assessment:
- Fine as an exploratory insight toy.
- Not strong enough to support meaningful “your data shows...” claims without more nuance.
Strengths:
- Pure, small, and easy to maintain.
- Avoids crashes and does basic paired-value hygiene correctly.
Gaps / risks:
- It only analyzes four hard-coded same-day pairs.
- It uses raw Pearson correlation with no lag modeling, no confound control, and a minimum of only seven paired points.
- The “Activity Minutes” insight is semantically misleading: in
apps/HeartCoach/Shared/Engine/CorrelationEngine.swift:91-100, it actually usesworkoutMinutesonly, not total activity minutes.- ✅ FIXED (2026-03-13): Changed keypath from
\.workoutMinutesto\.activityMinutes(computed property =walkMinutes + workoutMinutes). AddedactivityMinutestoHeartSnapshot.
- ✅ FIXED (2026-03-13): Changed keypath from
- The generated language can sound more causal and robust than the math really supports.
Verdict:
- Not enough for strong, trustworthy personalized insight cards.
- Even after the activity-minutes mapping fix, it still needs lag-aware analysis, broader factor coverage, and more careful language.
Assessment:
- Good product idea, weak current evidence model.
- Timing output is not reliable enough to be treated as truly personalized behavior learning.
Strengths:
- The decision tree is easy to reason about.
- It is a nice fit for watch/notification UX.
Gaps / bugs:
learnSleepPatterns()does not learn actual bedtimes/wake times from timestamped sleep sessions; it infers them from sleep duration heuristics alone inapps/HeartCoach/Shared/Engine/SmartNudgeScheduler.swift:74-84.bedtimeNudgeLeadMinutesandbreathPromptThresholdare declared but not used, which suggests intended behavior drift.recommendAction()usesDate()for bedtime day-of-week lookup inapps/HeartCoach/Shared/Engine/SmartNudgeScheduler.swift:240-243instead of using the provided context date. That hurts determinism and replayability.- ✅ FIXED (2026-03-13): Replaced
Date()withtodaySnapshot?.date ?? Date()for day-of-week lookup.
- ✅ FIXED (2026-03-13): Replaced
Verdict:
- Not enough for claims of learned personalized timing.
- Fine as a heuristic scheduler until the timing model uses real sleep/wake timestamps and the remaining unused config knobs are reconciled with actual behavior.
Assessment:
- The repo has enough data infrastructure for development, demos, regression testing, and one strong deep-dive validation area.
- It still does not have enough evenly distributed real validation coverage across all engines to justify strong confidence in repo-wide calibration.
- Deterministic synthetic personas in
apps/HeartCoach/Shared/Services/MockData.swift. - One real 32-day Apple Watch-derived sample embedded in
MockData.swift. - A validation harness in
apps/HeartCoach/Tests/Validation/DatasetValidationTests.swift. - A documented plan for external datasets in
apps/HeartCoach/Tests/Validation/FREE_DATASETS.md. - Real local stress-validation data now present under
apps/HeartCoach/Tests/Validation/Data/:swell_hrv.csvphysionet_exam_stress/WESAD.zipwesad_e4_mirror/
- A dedicated executed stress-validation write-up in STRESS_ENGINE_VALIDATION_REPORT.md.
- Outside the stress work, most engines still do not have equivalent executed real-data validation.
DatasetValidationTestsremain opt-in rather than part of the defaultswift testpath, because they depend on external datasets and Xcode-hosted execution.- Several iOS-only and external-data suites are still excluded from the default package run in
apps/HeartCoach/Package.swift. - There is still no held-out private product dataset with subjective labels for cross-engine calibration.
For development and regression:
- Yes, mostly.
- The synthetic personas and seeded histories are enough to keep core rules stable and deterministic.
For engine calibration and confidence in output quality:
- No.
- The synthetic data is partly circular: it encodes the same assumptions the engines reward, so passing those tests does not prove the rules generalize.
- The single embedded real-history sample is useful for demos and sanity checks, but it is still only one user and several fields are inferred/derived rather than ground-truth labeled.
- Stress is now the exception: it has moved beyond “aspirational” into an executed multi-dataset validation workflow.
- The broader repo is still uneven because the other engines have not yet reached that same validation maturity.
- Relative outputs are stronger than absolute outputs.
- Stronger: “today looks a bit better/worse than your normal,” “rest vs walk vs breathe,” “this week is elevated.”
- Weaker: exact stress score, exact bio age, exact weekly projections, true personalized bedtime timing, causal correlation insight text.
- Real multi-user Apple Watch histories, not just synthetic personas and one embedded export.
- A private evaluation set with subjective labels:
- perceived stress
- readiness/fatigue
- sleep quality
- illness/recovery events
- Longer longitudinal histories for the trend/coaching engines.
- Validation that actually runs in CI or a documented offline evaluation workflow.
- A separation between:
- synthetic regression data for deterministic behavior
- real calibration data for score quality
- held-out real evaluation data for final confidence checks
- Enough for a thoughtful prototype and for building the product loop.
- Not enough yet to say the engines are well-calibrated on real users across the repo.
- The biggest remaining gap is not code complexity; it is uneven real-data validation depth outside the now-stronger stress-engine workflow.
This is the practical guidance I would follow for creating datasets deeply enough to improve trust in the engines.
Do not rely on one dataset type.
You need three different dataset layers:
- Synthetic regression data
- Purpose: deterministic tests and edge cases
- Best for: preventing code regressions
- Not enough for: calibration confidence
- Real-world calibration data
- Purpose: tuning thresholds, score bands, and ranking behavior
- Best for: improving engine realism
- Not enough for: final confidence if reused for evaluation
- Held-out evaluation data
- Purpose: final verification on data the tuning process never saw
- Best for: measuring whether the engine generalizes
For this project, I would not call the datasets “deep enough” until they cover:
- multiple users, not one embedded Apple Watch export
- multiple weeks per user, not isolated daily samples
- multiple physiological states:
- normal baseline
- poor sleep
- high activity
- low activity
- stress-heavy periods
- recovery periods
- illness-like or fatigue-like periods when available
- both complete and incomplete data windows
- enough variation across:
- age
- sex
- fitness level
- work/rest routines
Goal:
- strengthen regression tests and scenario coverage
Recommended scope:
- 20-30 personas, not 10
- 60-90 days per persona, not 30
- explicit event injections:
- bad sleep streak
- exercise block
- sedentary week
- travel/jetlag-style sleep disruption
- illness/fatigue spike
- overtraining block
Important:
- keep synthetic data for test determinism
- do not treat it as evidence that the algorithms are calibrated correctly
Goal:
- tune engine thresholds and validate ranking behavior
Minimum useful target:
- 20-30 users
- 6-8 weeks each
- daily snapshots
- subjective labels at least 3-4 times per week
Better target:
- 50+ users
- 8-12 weeks each
Why:
- below that, the system can still be useful, but threshold tuning will stay fragile
Goal:
- verify generalization after tuning
Minimum useful target:
- 10-15 users completely excluded from tuning
- 4-8 weeks each
Better target:
- 20+ held-out users
Rule:
- no threshold or copy changes should be tuned on this set
Needs:
- long daily series per user
- stable baseline periods
- known disruptions
Good dataset depth:
- 28-60 days per user minimum
- enough missing days to test robustness
Labels to collect:
- “felt off today”
- “felt normal”
- illness/recovery notes if available
Needs:
- same-day physiological data plus subjective stress
Good dataset depth:
- 4+ weeks per user
- multiple stress and low-stress days per person
Best labels:
- perceived stress 1-5
- workload / exam / deadline flags
- sleep quality
Important:
- stress should be validated as a relative score first, not an absolute medical-grade score
Needs:
- sleep, recovery, activity, and subjective readiness/fatigue
Good dataset depth:
- 4-8 weeks per user
- at least several “good,” “average,” and “bad” recovery days per user
Best labels:
- “ready to train?” yes/no/low-medium-high
- fatigue score
- soreness score if available
Needs:
- this engine needs the deepest caution
Good dataset depth:
- large published reference tables plus real user data
- actual height if BMI is used
Best labels:
- use reference-norm benchmarking, not subjective “bio age” labels
Important:
- if real height is unavailable, do not over-invest in BMI-dependent conclusions
Needs:
- long time series with repeated routines
Good dataset depth:
- 8-12 weeks minimum
- enough behavior changes to compare before/after
Best labels:
- recommendation followed or not
- perceived benefit the next day
Needs:
- real zone-minute data, not empty arrays
Good dataset depth:
- per-workout zone distribution across multiple activity styles
- users with different ages and resting HR
Important:
this dataset is blocked until the HealthKit ingestion path actually populates✅ UNBLOCKED (commit 218b79b, CR-013)zoneMinutes
Needs:
- longer windows than the current 7-point minimum implies
Good dataset depth:
- 30-60 days per user minimum
- enough same-day and next-day pairs to test lag effects
Best labels:
- mostly internal validation rather than subjective labels
Important:
- use this as exploratory analytics, not hard truth
Needs:
- timestamped sleep/wake data
- interaction timestamps
Good dataset depth:
- several weeks per user
- weekday/weekend variation
- enough prompts to compare predicted timing vs actual user behavior
Best labels:
- prompt accepted/ignored
- time-to-action after prompt
For real datasets, I would standardize one canonical daily schema and one event schema.
Daily schema:
user_iddateresting_hrhrv_sdnnrecovery_hr_1mrecovery_hr_2mvo2_maxstepswalk_minutesworkout_minutessleep_hoursbody_mass_kgzone_minutes_1tozone_minutes_5wear_time_hoursmissingness_flags
Daily label schema:
perceived_stress_1_5readiness_1_5sleep_quality_1_5felt_sick_booltrained_today_boolrecommendation_followed_bool
Event schema:
user_idtimestampevent_typecontextengine_output_snapshotuser_feedback
I would only trust a dataset for calibration if it passes these quality rules:
- dates are continuous and timezone-normalized
- missing metrics are explicit, not silently zero-filled
- derived fields are tagged as derived, not mixed with raw observations
- wear-time is known or approximated
- there is enough per-user baseline depth before scoring trend-based outputs
Do not evaluate on the same users and periods used for tuning.
Recommended split:
- 60% calibration users
- 20% validation users
- 20% held-out evaluation users
If the sample is still small:
- split by user, not by day
- never mix one user’s days across train/eval if you want honest generalization estimates
For this project, I would call the datasets good enough only when:
- every core engine has deterministic synthetic regression coverage
- at least 20-30 real users are available for calibration
- at least 10 held-out users are available for evaluation
- real-data validation is runnable on demand and documented
- engine thresholds are tuned on calibration data and frozen before held-out evaluation
If I were prioritizing dataset work for the team, I would do it in this order:
- Expand synthetic personas and event injection for regression safety.
- Add a small but clean real-user calibration dataset with daily subjective labels.
- Build a separate held-out evaluation set before retuning thresholds.
- Only after that, revisit the absolute scores and more confident user-facing language.
The project already has many tests, but the biggest gaps are:
- integration tests for how engines are wired into the app
- regression tests for known edge cases in the current logic
- real-data validation that actually runs in a predictable workflow
Split tests into three layers:
- Fast default tests
- Run on every
swift test - Pure-engine unit tests
- Small integration tests with mock data
- No external datasets required
- Extended regression tests
- Time-series suites, persona suites, KPI suites
- Run in CI nightly or in a separate “extended” workflow
- Catch ranking drift and behavior regressions
- Dataset validation tests
- Real external data, opt-in
- Run with a documented local script or scheduled CI job when datasets are available
- Used for calibration confidence, not basic correctness
- Add a week-over-week non-overlap test:
- Current week has elevated RHR.
- Baseline period is stable and earlier.
- Assert the computed z-score reflects the full shift.
- This should catch the current overlapping-baseline bug.
- ✅ Underlying bug fixed:
HeartTrendEngine.swiftnow usesdropLast(currentWeekCount)to exclude current week from baseline. A regression test for this specific behavior is still recommended.
- Add a control test where only the current week changes:
- If the baseline excludes the current week, trend should move.
- If it includes the current week, trend will be artificially damped.
- Add a missing-day continuity test for
detectConsecutiveElevation():- Day 1 elevated, day 2 missing, day 3 elevated.
- Assert this does not count as 3 consecutive elevated days.
- Add threshold-edge tests:
- anomaly exactly at threshold
- regression slope exactly at threshold
- stress pattern with only 2 of 3 conditions true
- Add a stability test on sparse data:
- only one metric present across 14 days
- assert output remains low-confidence and non-crashing
✅ Test stabilization completed: Two flaky time-series tests were fixed by adjusting test data generator parameters (no engine changes):
-
testYoungAthleteLowStressAtDay30— reducedrhrNoisefrom 3.0 → 2.0 bpm inTests/EngineTimeSeries/TimeSeriesTestInfra.swift -
testNewMomVeryLowReadiness— lowered NewMomrecoveryHR1m18→15 andrecoveryHR2m25→22 inTests/EngineTimeSeries/TimeSeriesTestInfra.swift -
All 280 time-series checkpoint assertions now pass (140 stress + 140 readiness).
-
Add absolute calibration tests for the current algorithm:
- “healthy baseline day” should land in a bounded low-stress range
- “clearly elevated RHR day” should land in a bounded high-stress range
-
Add monotonicity tests:
- increasing RHR with all else fixed should never lower stress
- decreasing HRV with all else fixed should never lower stress
-
Add dominance tests:
- RHR-only stress event should still meaningfully raise score
- HRV-only anomaly should raise score less than matched RHR anomaly
-
Add sigmoid sanity tests:
- at-baseline score is inside the intended neutral band
- extreme inputs still clamp to
[0, 100]
-
Add replay tests against stored expected outputs for a few real-history windows from
MockData. -
If the external dataset is available:
- assert minimum separation between stressed and baseline groups
- store effect-size output in a machine-readable artifact
- Add integration tests for the actual app wiring:
- feed a real
StressResult.scoreinto readiness and compare against the current coarse70.0fallback path - assert the app path uses the richer signal once fixed
- ✅ Underlying bug fixed:
DashboardViewModel.computeReadiness()now callsStressEngine.compute()and passes the real score. File:apps/HeartCoach/iOS/ViewModels/DashboardViewModel.swift. A dedicated integration test asserting this path is still recommended.
- feed a real
- Add consecutive-alert cap tests:
- readiness without alert > 50
- same inputs with
consecutiveAlertcapped to<= 50
- Add pillar removal tests:
- sleep missing
- recovery missing
- stress missing
- assert correct re-normalization and no divide-by-zero
- Add activity-window behavior tests:
- one hard day followed by rest
- three sedentary days
- balanced weekly activity
- Add summary text tests:
- output should match level and not overstate certainty
- Add height-sensitivity tests:
- same weight and sex-average fallback vs realistic short/tall user once height exists
- this should expose the bias from average-height BMI approximation
- Add monotonicity tests:
- higher VO2 should not increase bio age
- lower RHR should not increase bio age
- higher HRV should not increase bio age
- Add plausibility-band tests:
- prevent unrealistic outputs from limited data
- assert result stays within a configurable range of chronological age unless multiple strong signals agree
- Add missing-data composition tests:
- exactly 2 metrics available
- 3 metrics available
- all 6 metrics available
- assert stable degradation, not discontinuous jumps
- Add reference-table tests:
- NTNU median should map near zero offset
- strong percentile cases should map in the expected direction
- Add end-to-end priority tests using full
HeartAssessmentbundles:- overtraining should outrank generic regression
- consecutive alert should outrank positive reinforcement
- Add dedupe tests across category collisions:
- multiple sources generating
.rest - assert only the highest-priority
.restrecommendation survives
- multiple sources generating
- Add uncertainty tests:
- low-confidence assessment should not generate overly strong recommendation copy if uncertainty messaging is added
- Add content-quality snapshot tests:
- recommendation title/message/detail for 5-10 representative scenarios
- this helps catch accidental wording regressions
- Add deterministic replay tests:
- pass a historical
current.date - assert “this week” and “last week” use the snapshot’s date context, not wall-clock
Date() - ✅ Underlying bug fixed:
CoachingEngine.generateReport()now usescurrent.dateinstead ofDate(). File:apps/HeartCoach/Shared/Engine/CoachingEngine.swift. A replay test asserting deterministic output is still recommended.
- pass a historical
- Add bounded-projection tests:
- projections should remain within realistic ranges
- no impossible negative exercise times or extreme multi-week gains
- Add sparse-history tests:
- 3 days, 7 days, 14 days
- assert the report degrades gracefully instead of producing overconfident text
- Add zone-dependency tests:
- no zone data should not imply zone-driven coaching
- Add report snapshot tests:
- same input history should always produce the same hero message and projections
- Add “why this nudge” tests:
- stress day
- regression day
- low-data day
- negative-feedback day
- high-readiness day
- recovering day
- Add anti-repetition tests:
- multiple consecutive days with similar inputs should still rotate within an allowed message set
- Add readiness-gating tests:
- low readiness must suppress moderate/high-intensity nudges
- high readiness should allow them
- Add copy-safety tests:
- no medical/diagnostic language
- no contradictory advice across primary and secondary nudges
- Add pipeline integration tests:
- verify real HealthKit-backed snapshots actually provide
zoneMinutesonce that path is implemented - fail if
zoneMinutesstays empty through the full snapshot path
- verify real HealthKit-backed snapshots actually provide
- Add formula tests for
computeZones():- zone bounds increase monotonically
- all zones are contiguous and non-overlapping
- values change sensibly with age and resting HR
- Add weekly-summary tests:
- moderate/vigorous target calculation
- AHA completion formula
- top-zone selection
- Add UI integration tests:
- dashboard should suppress zone messaging when no data exists
- dashboard should surface coaching only when real zone data is present
- Add semantic-correctness tests:
- if factor is labeled “Activity Minutes,” the underlying key path should include walk + workout, not workout only
- ✅ Underlying bug fixed:
CorrelationEngine.swiftnow uses\.activityMinutes(=walkMinutes + workoutMinutes) instead of\.workoutMinutes. File:apps/HeartCoach/Shared/Engine/CorrelationEngine.swift. Computed property added inapps/HeartCoach/Shared/Models/HeartModels.swift.
- Add lag tests:
- sleep today vs HRV tomorrow
- activity today vs recovery tomorrow
- even if not implemented yet, these should define the expected future direction
- Add confound-style tests:
- constant factor series
- sparse pair overlap
- outlier-heavy series
- Add language tests:
- interpretation must not imply causation when only correlation is measured
- Add threshold tests:
- exactly 6 paired points should not emit a result
- exactly 7 should
- Add behavior-learning tests using explicit timestamped sleep data once available.
- Add deterministic date-context tests:
- scheduler should use supplied date context, not
Date() - ✅ Underlying bug fixed:
SmartNudgeScheduler.recommendAction()now usestodaySnapshot?.date ?? Date()instead ofDate(). File:apps/HeartCoach/Shared/Engine/SmartNudgeScheduler.swift.
- scheduler should use supplied date context, not
- Add threshold-use tests:
breathPromptThresholdshould actually gate breath prompts if that is the intended designbedtimeNudgeLeadMinutesshould change output timing when modified
- Add fallback tests:
- low observation count should use defaults
- high observation count should use learned pattern
- Add late-wake tests:
- normal wake time
- slightly late but below threshold
- clearly late wake above threshold
- Add a separate validation command, for example:
swift test --filter DatasetValidationTests- or a dedicated script that first verifies required files exist
- Change dataset tests from “skip silently when missing” to “report clearly missing prerequisites” in the validation workflow summary.
- Add a manifest or JSON summary file for each validation run:
- dataset used
- row count
- effect size / AUC / correlation with labels
- pass/fail thresholds
- Add held-out benchmark fixtures:
- a few frozen real-world windows with expected engine outputs
- useful for catching unintentional recalibration drift
Promote some currently excluded suites into the default or extended CI path:✅ PROMOTED (commit 3e47b3d): Un-excluded from ThumpTests, now runs in defaultEngineKPIValidationTestsswift test.selected✅ PROMOTED (commit 3e47b3d): ThumpTimeSeriesTests target now includes EndToEnd, UICoherence, and MockProfile tests. Runs in defaultEngineTimeSeries/*swift test.DatasetValidationTestsin an opt-in validation job — ⬚ OPEN (needs external CSV data)
If only a few tests are added soon, I would prioritize these:
HeartTrendEngineweek-over-week non-overlap regression test — ✅ bug fixed inHeartTrendEngine.swift(dropLast), test still recommendedCoachingEnginedate-anchor replay test — ✅ bug fixed inCoachingEngine.swift(current.date), test still recommended✅ UNBLOCKED (commit 218b79b, CR-013):HeartRateZoneEnginepipeline test provingzoneMinutesare actually populated — ⬚ OPEN, blocked on HealthKit ingestionqueryZoneMinutes(for:)now ingests real workout HR samples. Test still recommended.ReadinessEngineintegration test using real stress score instead of the coarse70.0flag path — ✅ bug fixed inDashboardViewModel.swift, test still recommendedDatasetValidationTestsworkflow test that fails clearly when the validation job is misconfigured — ⬚ OPEN
Share the same✅ FIXED (commit 218b79b, PERF-4) — All view models now receive the shared HealthKitService viaHealthKitServiceinstance acrossDashboard,Insights,Stress, andTrendsinstead of each view model creating its own service instance.bind()from their views.- Avoid reloading HealthKit history on every range switch when a cached superset can serve multiple views. ⬚ OPEN
- Upsert stored daily history instead of append-only persistence. ✅ FIXED —
LocalStore.appendSnapshot(_:)now upserts by calendar day. File:apps/HeartCoach/Shared/Services/LocalStore.swift - Reduce warning noise in tests so performance regressions and real compiler warnings stand out faster. ✅ FIXED —
Package.swiftexclude list updated,ThumpBuddyFaceavailability guard added. Files:apps/HeartCoach/Package.swift,apps/HeartCoach/Shared/Views/ThumpBuddyFace.swift
Use batched HealthKit descriptors/collection queries instead of manual per-day fan-out.✅ FIXED (commit 218b79b, CR-005) —fetchHistory(days:)now usesHKStatisticsCollectionQueryfor RHR, HRV, steps, walkMinutes.- Introduce a dedicated per-day completion/streak model instead of overloading
WatchFeedbackPayload. ✅ FIXED — AddedlastStreakCreditDateandnudgeCompletionDatestoUserProfile. File:apps/HeartCoach/Shared/Models/HeartModels.swift Add app-level dependency injection for services that are currently instantiated ad hoc in view models.✅ IMPROVED (commit 218b79b, PERF-4) — All view models now usebind()pattern for shared service injection.Add integration tests for notification wiring, same-day refresh dedupe, and weekly completion accuracy.✅ PARTIALLY DONE — notification wiring complete (commitdcbee72), scheduling from live assessments wired. Integration tests for these paths are still recommended.
Fix the two data-integrity issues first: duplicate snapshot persistence and incorrect completion-rate accounting.✅ DONE — upsert inLocalStore.swift, completion tracking inHeartModels.swift+InsightsViewModel.swift+DashboardViewModel.swiftDecide whether notifications are a real shipping feature; if yes, wire✅ DONE (commit dcbee72) — Full notification pipeline: authorization + shared LocalStore + scheduling from live assessment output (anomaly alerts + smart nudge reminders).NotificationServicenow, otherwise remove or park it.Rework HealthKit history loading with batched queries before adding more views that depend on long lookback windows.✅ DONE (commit 218b79b, CR-005) —HKStatisticsCollectionQuerybatch queries for RHR, HRV, steps, walkMinutes.Prune or integrate orphaned services so the codebase reflects the actual runtime architecture.✅ DONE (commit 218b79b) —AlertMetricsService.swift,ConfigLoader.swift,File.swiftmoved to.unused/.WatchFeedbackBridge.swiftkept (likely needed for watch connectivity).
This section records findings identified and fixed during the hands-on coding session on 2026-03-13.
All dynamic Text views on watchOS that could produce long strings were missing lineLimit(nil) + fixedSize(horizontal: false, vertical: true). watchOS defaults to single-line truncation. Fixed six locations:
PlanScreen"Yet to Begin"pushMessagePlanScreensleep-modepushMessagePlanScreeninProgressMessageWalkNudgeScreenextraNudgeRowcontextual messageGoalProgressScreensleep-hour "Rest up" textSleepScreensleepSubMessageWatchDetailView"Sync with your iPhone..." placeholder
DatePicker used in: ...Date() allowing DOB of today (age = 0). Fixed to:
in: ...Calendar.current.date(byAdding: .year, value: -13, to: Date()) ?? Date()Also removed the force-unwrap ! on the date arithmetic result.
Both ViewModels had inner catch blocks that swallowed HealthKit errors before they reached the outer error handler:
DashboardViewModel: today snapshot fetch + history fetch both silently created empty data on deviceStressViewModel: history fetch silently returned[]on device
Fixed pattern in both (device #else branch now):
AppLogger.engine.error("... fetch failed: \(error.localizedDescription)")
errorMessage = "Unable to read health data. Please check Health permissions in Settings."
isLoading = false
returnThe errorMessage property drives the error UI in each view, so the user sees the failure instead of silently receiving wrong assessments.
Breathing session timer used a closure that could outlive self. Fixed with [weak self] in both the outer timer closure and the inner Task { @MainActor }, with explicit timer.invalidate() in the guard-nil path.
Snapshot history (HRV, RHR, steps, sleep) was stored in UserDefaults without application-level encryption. The existing CryptoService is now routed through the save()/load() helpers for this key.
DashboardViewModel called computeStress(snapshot:recentHistory:) while StressViewModel decomposed the snapshot and called computeStress(currentHRV:baselineHRV:). Same data could produce different scores. Both now use the unified computeStress(snapshot:recentHistory:) path.
try? checkVerification(result) was discarding the error silently. Unverified transactions are now explicitly logged via debugPrint before being skipped.
These issues were identified in this session but not yet fixed:
Both still do history = [] on device when HealthKit fails — same bug just fixed in Dashboard and Stress.
InsightsViewModel.swiftlines ~88-96TrendsViewModel.swiftlines ~128-136
Fix pattern (same as DashboardViewModel fix above):
#else
AppLogger.engine.error("... fetch failed: \(error.localizedDescription)")
errorMessage = "Unable to read health data. Please check Health permissions in Settings."
isLoading = false
return
#endifCreates expensive DateFormatter() instances inside functions called from ForEach loops:
iOS/Views/StressView.swift—formatWeekday(),formatDayHeader(),formatDate()(three separate inline formatters, called per heatmap cell)iOS/Views/InsightsView.swift—reportDateRange()(one inline formatter, called per weekly report card)iOS/Views/TrendsView.swift—xAxisLabels()(one inline formatter, called per chart render with a.map()loop)
Fix pattern for all three — replace inline creation with private static let:
private static let weekdayFormatter: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "EEE"; return f
}()"stress \(score), \(point!.level.displayName)" // point checked nil above but force-unwrappedUse if let p = point { ... } instead.
computeCorrelation() performs Pearson math inside a view helper called on every render. Should be memoized in TrendsViewModel and only recomputed when the underlying data changes.
checkCurrentAuthorization() only runs once at init. If the user grants or denies notification permission mid-session, the published property is never updated. Views should call checkCurrentAuthorization() in .onAppear.
- File:
iOS/Services/NotificationService.swift
// BUG-053: These fallback delivery hours are hardcoded defaults.
// TODO: Make configurable via Settings UIFile: iOS/Services/NotificationService.swift lines ~45-47. Move to issue tracker or implement.
queryRestingHeartRate(), queryHRV(), queryVO2Max() all return nil for both "no samples exist" and "query threw an error". The user cannot tell why a metric is missing.
Consider Result<Double?, HealthKitError> or a logged error companion alongside the nil return.