Showing 10 changed files with 55 additions and 45 deletions
+8 -8
HealthProbe/Doc/00-agent-guides/AGENTS.md
@@ -154,26 +154,26 @@ These types are defined once in `Models/` and shared across all agents:
154 154
 
155 155
 ```swift
156 156
 // Models/TypeDistributionBin.swift
157
-@Model
158
-final class TypeDistributionBin {
157
+struct TypeDistributionBin: Codable, Sendable {
159 158
     var bucketStart: Date
160 159
     var bucketEnd: Date
161 160
     var count: Int
162 161
 }
163 162
 
164 163
 // Models/TypeCount.swift
165
-// TypeCount owns zero or more TypeDistributionBin records.
166
-// These bins store sample counts and import anchors, not raw health values.
164
+// TypeCount stores yearly counts and distribution bins as compact Codable
165
+// payloads, not child SwiftData models.
167 166
 
168 167
 // Interface updated 2026-05-26 — see AGENTS.md
169 168
 // Models/HealthRecord.swift no longer defines a SwiftData model. It now keeps
170 169
 // the HealthRecordValue DTO plus compact archive helpers used by archive/cache
171 170
 // record previews. The authoritative record store is SQLite.
172 171
 
173
-// Interface updated 2026-05-13 — see AGENTS.md
174
-// TypeDistributionBin also stores content hashes and HealthKit query anchors.
175
-// Import uses a global anchored query per data type so follow-up snapshots fetch only
176
-// HealthKit deltas instead of scanning calendar blocks with fixed per-query latency.
172
+// Interface updated 2026-05-26 — see AGENTS.md
173
+// TypeDistributionBin and YearlyCount no longer define SwiftData models. They
174
+// are Codable payload structs embedded in TypeCount while capture is still on
175
+// the prototype bridge. Target archive/cache rows should come from SQLite/Core
176
+// Data instead.
177 177
 
178 178
 // Interface updated 2026-05-18 — see AGENTS.md
179 179
 // SwiftData is not the forensic source of truth and is legacy/prototype storage
+1 -1
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -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 8 SwiftData-backed files for launch container, capture review actions, capture bridge writes, and remaining model definitions.
51
+- Current UI/cache layers still depend on 6 SwiftData-backed files for launch container, capture review actions, 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.
+3 -2
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -268,8 +268,9 @@ Checklist:
268 268
   anomaly/count-drop review has been deleted; Snapshots/Data Types tab roots no
269 269
   longer import SwiftData; unused legacy snapshot/type detail and PDF views have
270 270
   been deleted; unused legacy lifecycle/observer/repair services have been
271
-  deleted; unused legacy delta service/models have been deleted; Dashboard
272
-  capture/review actions and capture bridge writes remain.
271
+  deleted; unused legacy delta service/models have been deleted; `HealthRecord`,
272
+  `YearlyCount`, and `TypeDistributionBin` are no longer SwiftData models;
273
+  Dashboard capture/review actions and capture bridge writes remain.
273 274
 - [ ] Remove/disable `ModelContainer` as required for target builds.
274 275
 - [x] Add prototype-store ignore/delete/reset path for test installs.
275 276
 - [ ] Verify no old-store compatibility layer remains in active flows.
+7 -5
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -10,7 +10,7 @@ local settings stored outside SwiftData where needed.
10 10
 ## Current Count
11 11
 
12 12
 After moving the Snapshots and Data Types tab roots to archive/cache
13
-observations and deleting unused legacy repair/detail/delta services, 8 app
13
+observations and deleting unused legacy repair/detail/delta services, 6 app
14 14
 files still have SwiftData imports because capture, Dashboard review actions,
15 15
 and remaining model definitions still use prototype snapshot handles.
16 16
 
@@ -31,12 +31,10 @@ block:
31 31
 
32 32
 - `HealthProbe/Models/HealthSnapshot.swift`
33 33
 - `HealthProbe/Models/TypeCount.swift`
34
-- `HealthProbe/Models/TypeDistributionBin.swift`
35
-- `HealthProbe/Models/YearlyCount.swift`
36 34
 
37 35
 Retirement path:
38
-- replace `HealthSnapshot`, `TypeCount`, `YearlyCount`,
39
-  and `TypeDistributionBin` active reads/writes with archive/cache DTOs;
36
+- replace `HealthSnapshot` and `TypeCount` active reads/writes with
37
+  archive/cache DTOs;
40 38
 - retire active reads/writes before removing the launch container.
41 39
 
42 40
 ## Capture And Maintenance Services
@@ -135,6 +133,10 @@ The following SwiftData dependencies were removed from active flows:
135 133
   `HealthProbe/Models/HealthRecord.swift` and removed from `ModelContainer`.
136 134
   The file now only contains the `HealthRecordValue` DTO and compact archive
137 135
   helpers used by archive/cache record previews.
136
+- `HealthProbe/Models/YearlyCount.swift` and
137
+  `HealthProbe/Models/TypeDistributionBin.swift` now define Codable payload
138
+  structs instead of SwiftData models. `TypeCount` stores those rows in compact
139
+  external-storage data fields while the capture bridge still exists.
138 140
 - The unused `HealthProbe/Views/DataTypes/TypeEvolutionTimeline.swift` legacy
139 141
   chart was deleted, and the remaining `TypeDiff`/`DiffFilter` DTOs now live in
140 142
   `HealthProbe/Models/TypeDiff.swift` instead of the removed
+2 -2
HealthProbe/HealthProbeApp.swift
@@ -27,7 +27,7 @@ struct HealthProbeApp: App {
27 27
         _ = try PrototypeStoreResetPolicy.applyIfNeeded()
28 28
 
29 29
         let fullSchema = Schema([
30
-            HealthSnapshot.self, TypeCount.self, YearlyCount.self, TypeDistributionBin.self,
30
+            HealthSnapshot.self, TypeCount.self,
31 31
         ])
32 32
 
33 33
         let appSupportURL = URL.applicationSupportDirectory
@@ -35,7 +35,7 @@ struct HealthProbeApp: App {
35 35
         let uiCacheStoreURL = appSupportURL.appending(path: "HealthProbeRecords.store")
36 36
 
37 37
         let uiCacheModels = Schema([
38
-            HealthSnapshot.self, TypeCount.self, YearlyCount.self, TypeDistributionBin.self,
38
+            HealthSnapshot.self, TypeCount.self,
39 39
         ])
40 40
 
41 41
         let uiCacheConfig = ModelConfiguration(
+25 -4
HealthProbe/Models/TypeCount.swift
@@ -15,11 +15,9 @@ import SwiftData
15 15
     var contentEquivalentTypeCountID: UUID?
16 16
     @Attribute(.externalStorage) var recordArchiveData: Data?
17 17
     @Attribute(.externalStorage) var detailCacheData: Data?
18
+    @Attribute(.externalStorage) var yearlyCountsData: Data?
19
+    @Attribute(.externalStorage) var distributionBinsData: Data?
18 20
     var snapshot: HealthSnapshot?
19
-    @Relationship(deleteRule: .cascade, inverse: \YearlyCount.typeCount)
20
-    var yearlyCounts: [YearlyCount]? = []
21
-    @Relationship(deleteRule: .cascade, inverse: \TypeDistributionBin.typeCount)
22
-    var distributionBins: [TypeDistributionBin]? = []
23 21
 
24 22
     init(typeIdentifier: String, displayName: String, count: Int, quality: SnapshotQuality = .complete) {
25 23
         self.id = UUID()
@@ -31,11 +29,34 @@ import SwiftData
31 29
 }
32 30
 
33 31
 extension TypeCount {
32
+    private static let propertyListEncoder = PropertyListEncoder()
33
+    private static let propertyListDecoder = PropertyListDecoder()
34
+
34 35
     var quality: SnapshotQuality {
35 36
         get { SnapshotQuality(rawValue: qualityRaw) ?? .complete }
36 37
         set { qualityRaw = newValue.rawValue }
37 38
     }
38 39
 
40
+    var yearlyCounts: [YearlyCount]? {
41
+        get {
42
+            guard let yearlyCountsData else { return [] }
43
+            return try? Self.propertyListDecoder.decode([YearlyCount].self, from: yearlyCountsData)
44
+        }
45
+        set {
46
+            yearlyCountsData = try? Self.propertyListEncoder.encode(newValue ?? [])
47
+        }
48
+    }
49
+
50
+    var distributionBins: [TypeDistributionBin]? {
51
+        get {
52
+            guard let distributionBinsData else { return [] }
53
+            return try? Self.propertyListDecoder.decode([TypeDistributionBin].self, from: distributionBinsData)
54
+        }
55
+        set {
56
+            distributionBinsData = try? Self.propertyListEncoder.encode(newValue ?? [])
57
+        }
58
+    }
59
+
39 60
     var recordValues: [HealthRecordValue] {
40 61
         if let recordArchiveData,
41 62
            let decoded = HealthRecordArchive.decode(recordArchiveData) {
+1 -5
HealthProbe/Models/TypeDistributionBin.swift
@@ -1,17 +1,13 @@
1 1
 import Foundation
2
-import SwiftData
3 2
 
4 3
 // Interface updated 2026-05-01 — see AGENTS.md
5
-@Model
6
-final class TypeDistributionBin {
4
+struct TypeDistributionBin: Codable, Identifiable, Hashable, Sendable {
7 5
     var id: UUID = UUID()
8 6
     var bucketStart: Date = Date.distantPast
9 7
     var bucketEnd: Date = Date.distantPast
10 8
     var count: Int = 0
11 9
     var contentHash: String = ""
12 10
     var anchorData: Data?
13
-    var typeCount: TypeCount?
14
-
15 11
     init(bucketStart: Date, bucketEnd: Date, count: Int) {
16 12
         self.id = UUID()
17 13
         self.bucketStart = bucketStart
+1 -4
HealthProbe/Models/YearlyCount.swift
@@ -1,14 +1,11 @@
1 1
 import Foundation
2
-import SwiftData
3 2
 
4
-@Model final class YearlyCount {
3
+struct YearlyCount: Codable, Identifiable, Hashable, Sendable {
5 4
     var id: UUID = UUID()
6 5
     var year: Int = 0
7 6
     var count: Int = 0
8 7
     var typeIdentifier: String = ""
9 8
     var isApproximate: Bool = false
10
-    var typeCount: TypeCount?
11
-
12 9
     init(year: Int, count: Int, typeIdentifier: String, isApproximate: Bool = false) {
13 10
         self.id = UUID()
14 11
         self.year = year
+6 -13
HealthProbe/Services/HealthKitService.swift
@@ -268,12 +268,6 @@ final class HealthKitService {
268 268
         context.insert(snapshot)
269 269
         for typeCount in typeCounts {
270 270
             context.insert(typeCount)
271
-            for yearlyCount in typeCount.yearlyCounts ?? [] {
272
-                context.insert(yearlyCount)
273
-            }
274
-            for bin in typeCount.distributionBins ?? [] {
275
-                context.insert(bin)
276
-            }
277 271
             typeCount.snapshot = snapshot
278 272
         }
279 273
         snapshot.typeCounts = typeCounts
@@ -371,7 +365,7 @@ final class HealthKitService {
371 365
             current.earliestDate = nil
372 366
             current.latestDate = nil
373 367
             current.quality = .unauthorized
374
-            current.yearlyCounts?.removeAll()
368
+            current.yearlyCounts = []
375 369
         }
376 370
     }
377 371
 
@@ -2006,25 +2000,24 @@ private struct TypeCountFetchResult: Sendable {
2006 2000
         typeCount.latestDate = latestDate
2007 2001
         typeCount.isUnsupported = isUnsupported
2008 2002
 
2009
-        for yearlyCountData in yearlyCounts {
2010
-            let yearlyCount = YearlyCount(
2003
+        typeCount.yearlyCounts = yearlyCounts.map { yearlyCountData in
2004
+            YearlyCount(
2011 2005
                 year: yearlyCountData.year,
2012 2006
                 count: yearlyCountData.count,
2013 2007
                 typeIdentifier: yearlyCountData.typeIdentifier,
2014 2008
                 isApproximate: yearlyCountData.isApproximate
2015 2009
             )
2016
-            typeCount.yearlyCounts?.append(yearlyCount)
2017 2010
         }
2018 2011
 
2019
-        for binData in distributionBins {
2020
-            let bin = TypeDistributionBin(
2012
+        typeCount.distributionBins = distributionBins.map { binData in
2013
+            var bin = TypeDistributionBin(
2021 2014
                 bucketStart: binData.bucketStart,
2022 2015
                 bucketEnd: binData.bucketEnd,
2023 2016
                 count: binData.count
2024 2017
             )
2025 2018
             bin.contentHash = binData.contentHash
2026 2019
             bin.anchorData = binData.anchorData
2027
-            typeCount.distributionBins?.append(bin)
2020
+            return bin
2028 2021
         }
2029 2022
 
2030 2023
         if let recordArchiveData {
+1 -1
HealthProbe/ViewModels/DashboardViewModel.swift
@@ -227,7 +227,7 @@ final class DashboardViewModel {
227 227
             typeCount.earliestDate = nil
228 228
             typeCount.latestDate = nil
229 229
             typeCount.quality = .unauthorized
230
-            typeCount.yearlyCounts?.removeAll()
230
+            typeCount.yearlyCounts = []
231 231
         }
232 232
         snapshot.snapshotQuality = healthKit.deriveSnapshotQuality(from: typeCounts)
233 233