Skip to content

Commit 5fb49e6

Browse files
committed
merge: competitor-benchmark — proactive notifications, clickability, 5-phase remediation
2 parents c898beb + d447b8e commit 5fb49e6

31 files changed

Lines changed: 5278 additions & 307 deletions

apps/HeartCoach/Package.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,17 @@ let package = Package(
4747
// SIGSEGV in testFullComparisonSummary (String(format: "%s") crash)
4848
"AlgorithmComparisonTests.swift",
4949
// EngineTimeSeries has its own target (ThumpTimeSeriesTests)
50-
"EngineTimeSeries"
50+
"EngineTimeSeries",
51+
// Firebase integration tests (need Firestore SDK, not in SPM target)
52+
"BugReportFirestoreTests.swift",
53+
"FeedbackFirestoreTests.swift",
54+
"FirestoreTelemetryIntegrationTests.swift",
55+
// Super Reviewer (needs Claude CLI + judge infrastructure)
56+
"SuperReviewer",
57+
// Proactive notifications (needs UNUserNotificationCenter, iOS-only)
58+
"ProactiveNotificationTests.swift",
59+
// Advice presenter copy fit (needs iOS Views)
60+
"AdvicePresenterCopyFitTests.swift"
5161
]
5262
),
5363
// TEST-3: Engine time-series validation suite (280 checkpoints).

apps/HeartCoach/README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,33 @@ This folder contains a cross-target scaffold for iPhone and Apple Watch:
1111
- For production, add Xcode targets and wire HealthKit/WatchConnectivity entitlements.
1212
- `project.yml` is included for `xcodegen`-based project generation.
1313

14-
## Run Core Tests
14+
## Run Tests
15+
16+
### Core unit tests (no Xcode required)
1517
```bash
16-
cd /Users/t/workspace/Apple-watch/apps/Thump
18+
cd apps/HeartCoach
1719
swift test
1820
```
1921

20-
## Generate Xcode Project (optional)
22+
### Engine time-series validation (280 checkpoints)
2123
```bash
22-
cd /Users/t/workspace/Apple-watch/apps/Thump
23-
xcodegen generate
24+
swift test --filter ThumpTimeSeriesTests
25+
```
26+
27+
### Full integration tests (requires Xcode + Simulator)
28+
```bash
29+
xcodebuild test -project Thump.xcodeproj -scheme Thump \
30+
-destination "platform=iOS Simulator,name=iPhone 17 Pro"
31+
```
32+
33+
### UI tests with granular gate control
34+
```bash
35+
# Full bypass (legacy)
36+
xcodebuild test ... -- -UITestMode
37+
38+
# Granular: test onboarding flow only
39+
xcodebuild test ... -- -UITest_SignedIn -UITest_LegalAccepted
40+
41+
# Granular: test legal gate only
42+
xcodebuild test ... -- -UITest_SignedIn
2443
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// ProactiveNotificationStore.swift
2+
// ThumpCore
3+
//
4+
// LocalStore extension for persisting proactive notification history.
5+
// Tracks when each notification type was last scheduled so the
6+
// ProactiveNotificationService can enforce budgets and cooldowns.
7+
//
8+
// Uses UserDefaults with a pruning strategy to prevent unbounded growth
9+
// (GPT-5.4 review fix #5: stale data persistence).
10+
//
11+
// Platforms: iOS 17+, watchOS 10+
12+
13+
import Foundation
14+
15+
// MARK: - LocalStore Extension
16+
17+
extension LocalStore {
18+
19+
// MARK: - Keys
20+
21+
private static let proactiveHistoryKey = "thump_proactive_notification_history"
22+
private static let maxHistoryDays = 14
23+
24+
// MARK: - Read
25+
26+
/// Returns all stored dates for a given notification type.
27+
func proactiveNotificationDates(for type: ProactiveNotificationType) -> [Date] {
28+
let all = loadProactiveHistory()
29+
return all[type.rawValue] ?? []
30+
}
31+
32+
// MARK: - Write
33+
34+
/// Records that a notification of the given type was scheduled at the given date.
35+
/// Automatically prunes entries older than 14 days to prevent unbounded growth.
36+
func logProactiveNotification(type: ProactiveNotificationType, at date: Date) {
37+
var all = loadProactiveHistory()
38+
var dates = all[type.rawValue] ?? []
39+
dates.append(date)
40+
41+
// Prune entries older than 14 days
42+
let cutoff = Calendar.current.date(
43+
byAdding: .day,
44+
value: -Self.maxHistoryDays,
45+
to: date
46+
) ?? date
47+
dates = dates.filter { $0 > cutoff }
48+
49+
all[type.rawValue] = dates
50+
saveProactiveHistory(all)
51+
}
52+
53+
// MARK: - Private Persistence
54+
55+
private func loadProactiveHistory() -> [String: [Date]] {
56+
guard let data = UserDefaults.standard.data(forKey: Self.proactiveHistoryKey),
57+
let decoded = try? JSONDecoder().decode([String: [Date]].self, from: data) else {
58+
return [:]
59+
}
60+
return decoded
61+
}
62+
63+
private func saveProactiveHistory(_ history: [String: [Date]]) {
64+
guard let data = try? JSONEncoder().encode(history) else { return }
65+
UserDefaults.standard.set(data, forKey: Self.proactiveHistoryKey)
66+
}
67+
}

apps/HeartCoach/Shared/Views/ThumpBuddy.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
// Platforms: iOS 17+, watchOS 10+
2323

2424
import SwiftUI
25+
#if canImport(UIKit)
26+
import UIKit
27+
#endif
2528

2629
// MARK: - Buddy Mood
2730

@@ -42,15 +45,38 @@ enum BuddyMood: String, Equatable, Sendable {
4245

4346
static func from(
4447
assessment: HeartAssessment,
48+
readinessScore: Int? = nil,
4549
nudgeCompleted: Bool = false,
4650
feedbackType: DailyFeedback? = nil,
4751
activityInProgress: Bool = false
4852
) -> BuddyMood {
4953
if nudgeCompleted { return .conquering }
5054
if feedbackType == .positive { return .conquering }
5155
if activityInProgress { return .active }
56+
57+
// Use readiness score as primary signal (coherent with Thump Check card).
58+
// Only show .tired when BOTH anomaly status AND readiness agree the user
59+
// should rest — prevents "Rest Up" contradicting "Good to go" (BUG-1).
60+
if let score = readinessScore {
61+
if score >= 80 { return .thriving }
62+
if score >= 60 {
63+
// Moderate-to-good readiness: show content unless stress is high
64+
return assessment.stressFlag ? .stressed : .content
65+
}
66+
if score >= 40 {
67+
// Below average: nudging toward recovery
68+
return assessment.stressFlag ? .stressed : .nudging
69+
}
70+
// Low readiness (< 40): genuinely tired — BUT only show sleeping
71+
// mood in evening hours. During daytime, show nudging instead.
72+
let hour = Calendar.current.component(.hour, from: Date())
73+
let isEvening = hour >= 20 || hour < 6
74+
return isEvening ? .tired : .nudging
75+
}
76+
77+
// Fallback for nil readiness (first run, no data)
5278
if assessment.stressFlag { return .stressed }
53-
if assessment.status == .needsAttention { return .tired }
79+
if assessment.status == .needsAttention { return .nudging }
5480
if assessment.status == .improving {
5581
if let cardio = assessment.cardioScore, cardio >= 70 { return .thriving }
5682
return .content
@@ -300,7 +326,7 @@ struct ThumpBuddy: View {
300326
// Haptic
301327
#if os(watchOS)
302328
WKInterfaceDevice.current().play(.click)
303-
#else
329+
#elseif canImport(UIKit)
304330
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
305331
#endif
306332

@@ -345,7 +371,7 @@ struct ThumpBuddy: View {
345371
// Haptic — soft
346372
#if os(watchOS)
347373
WKInterfaceDevice.current().play(.success)
348-
#else
374+
#elseif canImport(UIKit)
349375
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
350376
#endif
351377

0 commit comments

Comments
 (0)