@@ -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 |
@@ -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. |
@@ -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. |
@@ -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 |
@@ -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( |
@@ -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,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,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 |
@@ -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 {
|
@@ -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 |
|