Showing 5 changed files with 136 additions and 104 deletions
+4 -4
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -27,8 +27,8 @@ There are no real deployments, only test installations. Existing prototype datab
27 27
 | HealthKit capture | Capture now opens one archive observation per user-visible snapshot and attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it | Continue moving UI/cache reads to archive-backed observation ids |
28 28
 | SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed | Continue moving capture/Dashboard actions to archive/cache DTOs |
29 29
 | Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation |
30
-| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; replace capture/review actions before removing `ModelContainer` |
31
-| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and `DashboardView` no longer imports SwiftData or reads `ModelContext`; capture/review actions still route through a Dashboard view-model legacy bridge. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language | Move remaining Dashboard view-model capture/review actions away from SwiftData |
30
+| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view/view-model access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; stop returning/storing `HealthSnapshot` bridge handles before removing `ModelContainer` |
31
+| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and Dashboard view/view-model code no longer imports SwiftData or reads `ModelContext`; capture/review actions still route through a legacy bridge isolated in `HealthKitService`. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language | Stop returning/storing prototype `HealthSnapshot` handles from capture/review |
32 32
 | Diff/change explanation | SQL diff summaries, paged diff records, aggregate comparisons, and consolidation-evidence labels exist; legacy anomaly/count-drop review has been removed from active flows; the old direct `HealthSnapshot.typeCounts` diff helper has been retired | Keep active diff/count views on archive/cache DTOs |
33 33
 | Export | SQLite export preview, paged JSON writing, SHA256 manifest hashing, and `export_manifests` rows are in place for selected records and observation diffs | Fill remaining recovery-compatible envelope metadata, CSV export, relationship preservation, and reproducibility checks |
34 34
 | Legacy device support | Simplified detail UI mode is implemented for small/accessibility layouts and as a Settings toggle | Remove SwiftData dependency and validate lower deployment targets |
@@ -38,7 +38,7 @@ There are no real deployments, only test installations. Existing prototype datab
38 38
 
39 39
 Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.md).
40 40
 
41
-1. Move remaining Dashboard capture/review actions away from SwiftData.
41
+1. Stop returning/storing prototype `HealthSnapshot` handles from capture/review.
42 42
 2. Add targeted cache invalidation for affected observation/type ranges.
43 43
 3. Finish remaining UI language cleanup from anomaly/status to observation/diff/export where legacy model names still leak into active flows.
44 44
 4. Complete recovery-compatible export metadata, CSV output, and reproducibility checks.
@@ -48,7 +48,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
48 48
 
49 49
 - SwiftData currently blocks iOS 15-era device support.
50 50
 - Some screens still imply snapshot-count monitoring rather than Time Machine inspection.
51
-- Current UI/cache layers still depend on 5 SwiftData-backed files for launch container, capture review actions, capture bridge writes, and remaining model definitions.
51
+- Current UI/cache layers still depend on 4 SwiftData-backed files for launch container, capture bridge writes, and remaining model definitions.
52 52
 - Snapshots timeline/detail rows, Data Types list/detail rows, and record drill-down are archive/cache-backed for new archive v2 observations when cache rows exist.
53 53
 - Legacy SwiftData-only snapshots are reset for archive v2 test installs rather than migrated.
54 54
 - Capture strategy and some legacy SwiftData transition paths may still decode or cache too much data for low-end devices.
+10 -9
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -224,9 +224,9 @@ Acceptance:
224 224
 Checklist:
225 225
 - [ ] Replace direct SwiftData `@Query` dependencies for target screens.
226 226
 - [x] Dashboard status reads Core Data cached observation rows and cache health,
227
-  and `DashboardView` no longer imports SwiftData or reads `ModelContext`.
228
-  Capture/review actions still route through the Dashboard view model's legacy
229
-  bridge.
227
+  and Dashboard view/view-model code no longer imports SwiftData or reads
228
+  `ModelContext`. Capture/review actions still route through the legacy bridge
229
+  isolated in `HealthKitService`.
230 230
 - [x] Observation timeline rows read Core Data cache when available and no
231 231
   longer query `SnapshotDelta` list summaries.
232 232
 - [x] Observation root and archive detail use cached summary/type rows plus
@@ -268,12 +268,13 @@ Checklist:
268 268
   moved to local Codable stores and removed from `ModelContainer`; Settings
269 269
   data maintenance now uses the rebuildable Core Data cache; legacy
270 270
   anomaly/count-drop review has been deleted; Snapshots/Data Types tab roots no
271
-  longer import SwiftData; `DashboardView` no longer imports SwiftData or reads
272
-  `ModelContext`; unused legacy snapshot/type detail and PDF views have been
273
-  deleted; unused legacy lifecycle/observer/repair services have been deleted;
274
-  unused legacy delta service/models have been deleted; `HealthRecord`,
275
-  `YearlyCount`, and `TypeDistributionBin` are no longer SwiftData models;
276
-  Dashboard view-model capture/review actions and capture bridge writes remain.
271
+  longer import SwiftData; Dashboard view/view-model code no longer imports
272
+  SwiftData or reads `ModelContext`; unused legacy snapshot/type detail and PDF
273
+  views have been deleted; unused legacy lifecycle/observer/repair services have
274
+  been deleted; unused legacy delta service/models have been deleted;
275
+  `HealthRecord`, `YearlyCount`, and `TypeDistributionBin` are no longer
276
+  SwiftData models; capture/review still returns/stores prototype
277
+  `HealthSnapshot` handles through the legacy bridge.
277 278
 - [ ] Remove/disable `ModelContainer` as required for target builds.
278 279
 - [x] Add prototype-store ignore/delete/reset path for test installs.
279 280
 - [ ] Verify no old-store compatibility layer remains in active flows.
+14 -13
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -9,10 +9,10 @@ local settings stored outside SwiftData where needed.
9 9
 
10 10
 ## Current Count
11 11
 
12
-After moving the Snapshots, Data Types, and Dashboard view roots to
13
-archive/cache observations or view-model bridges, 5 app files still have
14
-SwiftData imports because capture, Dashboard review actions, and remaining model
15
-definitions still use prototype snapshot handles.
12
+After moving the Snapshots, Data Types, Dashboard view, and Dashboard view model
13
+roots away from direct SwiftData imports, 4 app files still have SwiftData
14
+imports because launch, capture bridge writes, and remaining model definitions
15
+still use prototype snapshot handles.
16 16
 
17 17
 ## Launch Container
18 18
 
@@ -49,13 +49,14 @@ Retirement path:
49 49
 
50 50
 ## UI And View Models
51 51
 
52
-These active surfaces still use `@Query`, `ModelContext`, or SwiftData model
53
-types:
54
-
55
-- `HealthProbe/ViewModels/DashboardViewModel.swift`
52
+No active UI/view-model file currently imports SwiftData. Dashboard
53
+capture/review actions still hold legacy `HealthSnapshot` handles while the
54
+bridge exists, but `ModelContext` and `FetchDescriptor` access is isolated in
55
+`HealthKitService`.
56 56
 
57 57
 Retirement path:
58
-- move capture/review actions away from `ModelContext`;
58
+- stop returning/storing prototype `HealthSnapshot` handles from capture/review
59
+  flows;
59 60
 - keep status/report rows on archive/cache APIs.
60 61
 
61 62
 ## Removed During This Pass
@@ -83,12 +84,12 @@ The following SwiftData dependencies were removed from active flows:
83 84
 - `HealthProbe/Views/Settings/SettingsView.swift` no longer imports SwiftData.
84 85
   Its Data section now reports/rebuilds/deletes the rebuildable Core Data UI
85 86
   cache and leaves the SQLite archive untouched.
86
-- `HealthProbe/Views/Dashboard/DashboardView.swift` no longer queries
87
-  `HealthSnapshot` for status rows; Dashboard status now uses archive/cache rows
88
-  only. SwiftData remains there for capture/review actions.
89 87
 - `HealthProbe/Views/Dashboard/DashboardView.swift` no longer imports SwiftData
90 88
   or reads `ModelContext`. Capture/review actions now route through the
91
-  Dashboard view model's legacy bridge while that bridge is retired.
89
+  Dashboard view model while the legacy bridge is retired.
90
+- `HealthProbe/ViewModels/DashboardViewModel.swift` no longer imports SwiftData
91
+  or reads `ModelContext`/`FetchDescriptor`. Legacy snapshot-cache operations
92
+  are isolated in `HealthKitService`.
92 93
 - `HealthProbe/Views/DataTypes/RecordChangeEvolutionChart.swift` now accepts a
93 94
   small `RecordChangeEvolutionSnapshot` DTO and loads archive/cache counts
94 95
   without importing SwiftData or querying `SnapshotDelta`.
+94 -0
HealthProbe/Services/HealthKitService.swift
@@ -35,6 +35,100 @@ extension Array where Element == MonitoredType {
35 35
     }
36 36
 }
37 37
 
38
+enum LegacySwiftDataBridgeError: LocalizedError {
39
+    case notConfigured
40
+
41
+    var errorDescription: String? {
42
+        switch self {
43
+        case .notConfigured:
44
+            return "Legacy SwiftData bridge is not configured."
45
+        }
46
+    }
47
+}
48
+
49
+enum LegacySwiftDataBridge {
50
+    private static var container: ModelContainer?
51
+    private static var context: ModelContext?
52
+
53
+    static func configure(container: ModelContainer) {
54
+        self.container = container
55
+        context = ModelContext(container)
56
+    }
57
+
58
+    static func createSnapshot(
59
+        using healthKit: HealthKitService,
60
+        selectedTypeIDs: Set<String>,
61
+        adaptiveTimeoutsEnabled: Bool,
62
+        triggerReason: String,
63
+        retryOfSnapshotID: UUID?,
64
+        timeoutMultiplier: Double,
65
+        reviewAmbiguousCompleteDisappearedTypes: Bool,
66
+        progress: SnapshotFetchProgress?
67
+    ) async throws -> HealthSnapshot {
68
+        try await healthKit.createSnapshot(
69
+            in: modelContext(),
70
+            selectedTypeIDs: selectedTypeIDs,
71
+            adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled,
72
+            triggerReason: triggerReason,
73
+            retryOfSnapshotID: retryOfSnapshotID,
74
+            timeoutMultiplier: timeoutMultiplier,
75
+            reviewAmbiguousCompleteDisappearedTypes: reviewAmbiguousCompleteDisappearedTypes,
76
+            progress: progress
77
+        )
78
+    }
79
+
80
+    static func savePartialSnapshot(
81
+        _ snapshot: HealthSnapshot,
82
+        using healthKit: HealthKitService
83
+    ) async throws -> HealthSnapshot {
84
+        try await healthKit.savePartialSnapshot(snapshot, in: modelContext())
85
+    }
86
+
87
+    static func saveReviewedCompleteSnapshot(
88
+        _ snapshot: HealthSnapshot,
89
+        using healthKit: HealthKitService
90
+    ) async throws -> HealthSnapshot {
91
+        try await healthKit.saveReviewedCompleteSnapshot(snapshot, in: modelContext())
92
+    }
93
+
94
+    static func snapshotExists(id: UUID) throws -> Bool {
95
+        var descriptor = FetchDescriptor<HealthSnapshot>(
96
+            predicate: #Predicate<HealthSnapshot> { $0.id == id }
97
+        )
98
+        descriptor.fetchLimit = 1
99
+        return try !modelContext().fetch(descriptor).isEmpty
100
+    }
101
+
102
+    static func previousSnapshot(for snapshot: HealthSnapshot) -> HealthSnapshot? {
103
+        guard let previousID = snapshot.previousSnapshotID else { return nil }
104
+        let descriptor = FetchDescriptor<HealthSnapshot>(
105
+            predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
106
+        )
107
+        return try? modelContext().fetch(descriptor).first
108
+    }
109
+
110
+    static func deleteSnapshot(id: UUID) throws {
111
+        let context = try modelContext()
112
+        var descriptor = FetchDescriptor<HealthSnapshot>(
113
+            predicate: #Predicate<HealthSnapshot> { $0.id == id }
114
+        )
115
+        descriptor.fetchLimit = 1
116
+        if let snapshot = try context.fetch(descriptor).first {
117
+            context.delete(snapshot)
118
+        }
119
+    }
120
+
121
+    private static func modelContext() throws -> ModelContext {
122
+        if let context { return context }
123
+        guard let container else {
124
+            throw LegacySwiftDataBridgeError.notConfigured
125
+        }
126
+        let context = ModelContext(container)
127
+        self.context = context
128
+        return context
129
+    }
130
+}
131
+
38 132
 final class HealthKitService {
39 133
     static let shared = HealthKitService()
40 134
     let store = HKHealthStore()
+14 -78
HealthProbe/ViewModels/DashboardViewModel.swift
@@ -1,36 +1,4 @@
1 1
 import Foundation
2
-import SwiftData
3
-
4
-enum LegacySwiftDataBridgeError: LocalizedError {
5
-    case notConfigured
6
-
7
-    var errorDescription: String? {
8
-        switch self {
9
-        case .notConfigured:
10
-            return "Legacy SwiftData bridge is not configured."
11
-        }
12
-    }
13
-}
14
-
15
-enum LegacySwiftDataBridge {
16
-    private static var container: ModelContainer?
17
-    private static var context: ModelContext?
18
-
19
-    static func configure(container: ModelContainer) {
20
-        self.container = container
21
-        context = ModelContext(container)
22
-    }
23
-
24
-    static func modelContext() throws -> ModelContext {
25
-        if let context { return context }
26
-        guard let container else {
27
-            throw LegacySwiftDataBridgeError.notConfigured
28
-        }
29
-        let context = ModelContext(container)
30
-        self.context = context
31
-        return context
32
-    }
33
-}
34 2
 
35 3
 @Observable
36 4
 final class DashboardViewModel {
@@ -86,16 +54,6 @@ final class DashboardViewModel {
86 54
             return
87 55
         }
88 56
 
89
-        let context: ModelContext
90
-        do {
91
-            context = try LegacySwiftDataBridge.modelContext()
92
-        } catch {
93
-            snapshotError = "Failed to access legacy snapshot cache: \(error.localizedDescription)"
94
-            snapshotProgress = .idle
95
-            showProgressSheet = true
96
-            return
97
-        }
98
-
99 57
         isCreatingSnapshot = true
100 58
         snapshotError = nil
101 59
         snapshotProgress = .fetching
@@ -135,8 +93,8 @@ final class DashboardViewModel {
135 93
             let learnedMetricTimeout = Double(selectedTypeIDs.count) * HealthKitService.maximumTimeoutSeconds + 30
136 94
             let operationTimeout = max(120, historyImportTimeout, learnedMetricTimeout)
137 95
             let snapshot = try await withTimeout(seconds: operationTimeout) {
138
-                try await self.healthKit.createSnapshot(
139
-                    in: context,
96
+                try await LegacySwiftDataBridge.createSnapshot(
97
+                    using: self.healthKit,
140 98
                     selectedTypeIDs: selectedTypeIDs,
141 99
                     adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled,
142 100
                     triggerReason: triggerReason,
@@ -177,7 +135,7 @@ final class DashboardViewModel {
177 135
                 snapshotProgressMessage = "Incomplete snapshot"
178 136
                 permissionsAlreadyRequested = healthKit.hasRequestedPermissionsBefore
179 137
 
180
-                if shouldAutoSaveKnownUnauthorizedPartial(snapshot: snapshot, failedCount: failedCount, context: context) {
138
+                if shouldAutoSaveKnownUnauthorizedPartial(snapshot: snapshot, failedCount: failedCount) {
181 139
                     snapshotProgressMessage = "Known unavailable metrics"
182 140
                     snapshotProgressDetail = "Snapshot was auto-saved as partial because unavailable metrics were already confirmed in the previous snapshot."
183 141
                     await savePartialSnapshot(keepSheetOpenForReview: true)
@@ -217,7 +175,7 @@ final class DashboardViewModel {
217 175
                 return
218 176
             }
219 177
 
220
-            let ambiguousMetrics = findAmbiguousDisappearedMetrics(snapshot: snapshot, context: context)
178
+            let ambiguousMetrics = findAmbiguousDisappearedMetrics(snapshot: snapshot)
221 179
             if !ambiguousMetrics.isEmpty {
222 180
                 pendingAmbiguousSnapshot = snapshot
223 181
                 ambiguousDisappearedMetrics = ambiguousMetrics
@@ -229,11 +187,7 @@ final class DashboardViewModel {
229 187
             }
230 188
 
231 189
             let snapshotID = snapshot.id
232
-            var descriptor = FetchDescriptor<HealthSnapshot>(
233
-                predicate: #Predicate<HealthSnapshot> { $0.id == snapshotID }
234
-            )
235
-            descriptor.fetchLimit = 1
236
-            let exists = try !context.fetch(descriptor).isEmpty
190
+            let exists = try LegacySwiftDataBridge.snapshotExists(id: snapshotID)
237 191
 
238 192
             if !exists {
239 193
                 throw SnapshotCreationError.snapshotNotSaved
@@ -272,8 +226,7 @@ final class DashboardViewModel {
272 226
         snapshot.snapshotQuality = healthKit.deriveSnapshotQuality(from: typeCounts)
273 227
 
274 228
         do {
275
-            let context = try LegacySwiftDataBridge.modelContext()
276
-            let saved = try await healthKit.savePartialSnapshot(snapshot, in: context)
229
+            let saved = try await LegacySwiftDataBridge.savePartialSnapshot(snapshot, using: healthKit)
277 230
             finishSavedReviewedSnapshot(saved)
278 231
         } catch {
279 232
             snapshotError = "Failed to save reviewed snapshot: \(error.localizedDescription)"
@@ -285,8 +238,7 @@ final class DashboardViewModel {
285 238
         guard let snapshot = pendingAmbiguousSnapshot else { return }
286 239
 
287 240
         do {
288
-            let context = try LegacySwiftDataBridge.modelContext()
289
-            let saved = try await healthKit.saveReviewedCompleteSnapshot(snapshot, in: context)
241
+            let saved = try await LegacySwiftDataBridge.saveReviewedCompleteSnapshot(snapshot, using: healthKit)
290 242
             finishSavedReviewedSnapshot(saved)
291 243
         } catch {
292 244
             snapshotError = "Failed to save reviewed snapshot: \(error.localizedDescription)"
@@ -337,8 +289,7 @@ final class DashboardViewModel {
337 289
         }
338 290
 
339 291
         do {
340
-            let context = try LegacySwiftDataBridge.modelContext()
341
-            let saved = try await healthKit.savePartialSnapshot(snapshot, in: context)
292
+            let saved = try await LegacySwiftDataBridge.savePartialSnapshot(snapshot, using: healthKit)
342 293
             completedSnapshotID = saved.id
343 294
             completedSnapshotTimestamp = saved.timestamp
344 295
             completedSnapshotDeviceID = saved.deviceID
@@ -376,14 +327,7 @@ final class DashboardViewModel {
376 327
         ambiguousDisappearedMetrics = []
377 328
         if let snapshotID = completedSnapshotID {
378 329
             do {
379
-                let context = try LegacySwiftDataBridge.modelContext()
380
-                var descriptor = FetchDescriptor<HealthSnapshot>(
381
-                    predicate: #Predicate<HealthSnapshot> { $0.id == snapshotID }
382
-                )
383
-                descriptor.fetchLimit = 1
384
-                if let snapshot = try context.fetch(descriptor).first {
385
-                    context.delete(snapshot)
386
-                }
330
+                try LegacySwiftDataBridge.deleteSnapshot(id: snapshotID)
387 331
             } catch { }
388 332
         }
389 333
         completedSnapshotID = nil
@@ -420,13 +364,8 @@ final class DashboardViewModel {
420 364
         }
421 365
     }
422 366
 
423
-    private func findAmbiguousDisappearedMetrics(snapshot: HealthSnapshot, context: ModelContext) -> [AmbiguousDisappearedMetric] {
424
-        guard let previousID = snapshot.previousSnapshotID else { return [] }
425
-        let descriptor = FetchDescriptor<HealthSnapshot>(
426
-            predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
427
-        )
428
-        guard let previous = try? context.fetch(descriptor).first else { return [] }
429
-
367
+    private func findAmbiguousDisappearedMetrics(snapshot: HealthSnapshot) -> [AmbiguousDisappearedMetric] {
368
+        guard let previous = LegacySwiftDataBridge.previousSnapshot(for: snapshot) else { return [] }
430 369
         let previousByType = Dictionary(
431 370
             uniqueKeysWithValues: (previous.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
432 371
         )
@@ -454,19 +393,16 @@ final class DashboardViewModel {
454 393
         }
455 394
     }
456 395
 
457
-    private func shouldAutoSaveKnownUnauthorizedPartial(snapshot: HealthSnapshot, failedCount: Int, context: ModelContext) -> Bool {
396
+    private func shouldAutoSaveKnownUnauthorizedPartial(snapshot: HealthSnapshot, failedCount: Int) -> Bool {
458 397
         guard failedCount == 0,
459
-              let previousID = snapshot.previousSnapshotID else {
398
+              snapshot.previousSnapshotID != nil else {
460 399
             return false
461 400
         }
462 401
 
463 402
         let currentUnauthorized = (snapshot.typeCounts ?? []).filter { $0.quality == .unauthorized }
464 403
         guard !currentUnauthorized.isEmpty else { return false }
465 404
 
466
-        let descriptor = FetchDescriptor<HealthSnapshot>(
467
-            predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
468
-        )
469
-        guard let previous = try? context.fetch(descriptor).first else {
405
+        guard let previous = LegacySwiftDataBridge.previousSnapshot(for: snapshot) else {
470 406
             return false
471 407
         }
472 408