@@ -53,6 +53,12 @@ Do not implement one-way migration from the old prototype schema unless a later |
||
| 53 | 53 |
|
| 54 | 54 |
During the SwiftData-to-archive-v2 transition, legacy SwiftData snapshots must not be mixed with new SQLite archive observations. A SwiftData snapshot that predates archive v2 can still produce a UI diff, but its historical records may not exist in the archive and therefore cannot be exported or used as backup evidence. Test builds may destructively reset `HealthProbeRecords.store`, `HealthProbeArchive.sqlite`, and `HealthProbeCache.sqlite` once for this transition. Local settings stores can be preserved. |
| 55 | 55 |
|
| 56 |
+The manual test reset is scheduled with both a `UserDefaults` flag and a small |
|
| 57 |
+marker file in Application Support. Startup reset must treat either signal as |
|
| 58 |
+authoritative, delete the archive/cache/prototype stores and SQLite sidecars, |
|
| 59 |
+then clear both signals. This keeps benchmark resets reliable even if a test |
|
| 60 |
+device is force-closed immediately after scheduling. |
|
| 61 |
+ |
|
| 56 | 62 |
## 3. HealthKit Capture |
| 57 | 63 |
|
| 58 | 64 |
Use: |
@@ -196,6 +196,7 @@ captures. |
||
| 196 | 196 |
| 2026-06-02 | `ff59257` | Removed unused `samples` indexes on global UUID hash and semantic fingerprint. | Awaiting comparable first-import report. Expected signal is lower `SummedInsertElapsed`; deleted-object lookup remains covered by `(sample_type_id, sample_uuid_hash)`. | |
| 197 | 197 |
| 2026-06-02 | pending | Captured non-chain-start full-scan report after index removal. | Not comparable for first-import performance; reveals a separate full-scan/unchanged-sample write bottleneck. | |
| 198 | 198 |
| 2026-06-02 | pending | Stopped writing `verified` observation events for unchanged existing samples. | Awaiting comparable non-chain-start/full-scan report. Expected signal is lower `SummedInsertElapsed` and especially lower Heart Rate insert time when most rows are unchanged. | |
| 199 |
+| 2026-06-02 | pending | Fortified scheduled test database reset with a disk marker and extra SQLite sidecar cleanup. | Awaiting real-device confirmation that reset survives force-close/relaunch and produces a clean first-snapshot timeline. | |
|
| 199 | 200 |
|
| 200 | 201 |
## Current Diagnosis |
| 201 | 202 |
|
@@ -9,14 +9,32 @@ enum PrototypeStoreResetPolicy {
|
||
| 9 | 9 |
static let currentGeneration = 1 |
| 10 | 10 |
static let defaultsKey = "hp_prototypeStoreResetGeneration" |
| 11 | 11 |
static let manualResetDefaultsKey = "hp_prototypeStoreResetScheduled" |
| 12 |
+ static let manualResetMarkerFileName = ".HealthProbeResetScheduled" |
|
| 12 | 13 |
|
| 13 |
- static func isResetScheduled(defaults: UserDefaults = .standard) -> Bool {
|
|
| 14 |
- defaults.bool(forKey: manualResetDefaultsKey) || defaults.integer(forKey: defaultsKey) < currentGeneration |
|
| 14 |
+ static func isResetScheduled( |
|
| 15 |
+ defaults: UserDefaults = .standard, |
|
| 16 |
+ appSupportURL: URL? = nil, |
|
| 17 |
+ fileManager: FileManager = .default |
|
| 18 |
+ ) -> Bool {
|
|
| 19 |
+ if defaults.bool(forKey: manualResetDefaultsKey) || defaults.integer(forKey: defaultsKey) < currentGeneration {
|
|
| 20 |
+ return true |
|
| 21 |
+ } |
|
| 22 |
+ guard let appSupportURL else { return false }
|
|
| 23 |
+ return fileManager.fileExists(atPath: manualResetMarkerURL(appSupportURL: appSupportURL).path) |
|
| 15 | 24 |
} |
| 16 | 25 |
|
| 17 |
- static func requestResetOnNextLaunch(defaults: UserDefaults = .standard) {
|
|
| 26 |
+ static func requestResetOnNextLaunch( |
|
| 27 |
+ defaults: UserDefaults = .standard, |
|
| 28 |
+ appSupportURL: URL = .applicationSupportDirectory, |
|
| 29 |
+ fileManager: FileManager = .default |
|
| 30 |
+ ) {
|
|
| 18 | 31 |
defaults.set(true, forKey: manualResetDefaultsKey) |
| 19 | 32 |
defaults.synchronize() |
| 33 |
+ try? fileManager.createDirectory(at: appSupportURL, withIntermediateDirectories: true) |
|
| 34 |
+ try? Data(Date().ISO8601Format().utf8).write( |
|
| 35 |
+ to: manualResetMarkerURL(appSupportURL: appSupportURL), |
|
| 36 |
+ options: [.atomic] |
|
| 37 |
+ ) |
|
| 20 | 38 |
} |
| 21 | 39 |
|
| 22 | 40 |
static func applyIfNeeded( |
@@ -24,7 +42,7 @@ enum PrototypeStoreResetPolicy {
|
||
| 24 | 42 |
defaults: UserDefaults = .standard, |
| 25 | 43 |
fileManager: FileManager = .default |
| 26 | 44 |
) throws -> PrototypeStoreResetResult {
|
| 27 |
- guard isResetScheduled(defaults: defaults) else {
|
|
| 45 |
+ guard isResetScheduled(defaults: defaults, appSupportURL: appSupportURL, fileManager: fileManager) else {
|
|
| 28 | 46 |
return PrototypeStoreResetResult(didReset: false, removedURLs: []) |
| 29 | 47 |
} |
| 30 | 48 |
|
@@ -38,6 +56,7 @@ enum PrototypeStoreResetPolicy {
|
||
| 38 | 56 |
defaults.set(currentGeneration, forKey: defaultsKey) |
| 39 | 57 |
defaults.set(false, forKey: manualResetDefaultsKey) |
| 40 | 58 |
defaults.synchronize() |
| 59 |
+ try? fileManager.removeItem(at: manualResetMarkerURL(appSupportURL: appSupportURL)) |
|
| 41 | 60 |
return PrototypeStoreResetResult(didReset: true, removedURLs: removedURLs) |
| 42 | 61 |
} |
| 43 | 62 |
|
@@ -60,9 +79,15 @@ enum PrototypeStoreResetPolicy {
|
||
| 60 | 79 |
baseURL, |
| 61 | 80 |
appSupportURL.appending(path: "\(baseName)-shm"), |
| 62 | 81 |
appSupportURL.appending(path: "\(baseName)-wal"), |
| 82 |
+ appSupportURL.appending(path: "\(baseName)-journal"), |
|
| 63 | 83 |
appSupportURL.appending(path: "\(baseName).shm"), |
| 64 |
- appSupportURL.appending(path: "\(baseName).wal") |
|
| 84 |
+ appSupportURL.appending(path: "\(baseName).wal"), |
|
| 85 |
+ appSupportURL.appending(path: "\(baseName).journal") |
|
| 65 | 86 |
] |
| 66 | 87 |
} |
| 67 | 88 |
} |
| 89 |
+ |
|
| 90 |
+ private static func manualResetMarkerURL(appSupportURL: URL) -> URL {
|
|
| 91 |
+ appSupportURL.appending(path: manualResetMarkerFileName) |
|
| 92 |
+ } |
|
| 68 | 93 |
} |
@@ -9,7 +9,7 @@ struct SettingsView: View {
|
||
| 9 | 9 |
@State private var archiveObservationCount: Int? |
| 10 | 10 |
@State private var timeoutProfiles: [LocalMetricTimeoutProfile] = [] |
| 11 | 11 |
@State private var currentDeviceProfile: LocalDeviceProfile? |
| 12 |
- @State private var resetScheduled = PrototypeStoreResetPolicy.isResetScheduled() |
|
| 12 |
+ @State private var resetScheduled = PrototypeStoreResetPolicy.isResetScheduled(appSupportURL: .applicationSupportDirectory) |
|
| 13 | 13 |
|
| 14 | 14 |
private var currentDeviceID: String {
|
| 15 | 15 |
AppSettings.currentDeviceID |
@@ -283,7 +283,7 @@ struct SettingsView: View {
|
||
| 283 | 283 |
} |
| 284 | 284 |
|
| 285 | 285 |
private func loadArchiveCacheStatus() {
|
| 286 |
- resetScheduled = PrototypeStoreResetPolicy.isResetScheduled() |
|
| 286 |
+ resetScheduled = PrototypeStoreResetPolicy.isResetScheduled(appSupportURL: .applicationSupportDirectory) |
|
| 287 | 287 |
do {
|
| 288 | 288 |
archiveObservationCount = try CoreDataArchiveCacheStore().observationCount() |
| 289 | 289 |
} catch {
|
@@ -64,7 +64,10 @@ final class PrototypeStoreResetPolicyTests: XCTestCase {
|
||
| 64 | 64 |
|
| 65 | 65 |
XCTAssertFalse(PrototypeStoreResetPolicy.isResetScheduled(defaults: defaults)) |
| 66 | 66 |
|
| 67 |
- PrototypeStoreResetPolicy.requestResetOnNextLaunch(defaults: defaults) |
|
| 67 |
+ PrototypeStoreResetPolicy.requestResetOnNextLaunch( |
|
| 68 |
+ defaults: defaults, |
|
| 69 |
+ appSupportURL: temporaryDirectory |
|
| 70 |
+ ) |
|
| 68 | 71 |
|
| 69 | 72 |
XCTAssertTrue(PrototypeStoreResetPolicy.isResetScheduled(defaults: defaults)) |
| 70 | 73 |
} |
@@ -73,10 +76,15 @@ final class PrototypeStoreResetPolicyTests: XCTestCase {
|
||
| 73 | 76 |
defaults.set(PrototypeStoreResetPolicy.currentGeneration, forKey: PrototypeStoreResetPolicy.defaultsKey) |
| 74 | 77 |
let archiveURL = temporaryDirectory.appending(path: "HealthProbeArchive.sqlite") |
| 75 | 78 |
let cacheWALURL = temporaryDirectory.appending(path: "HealthProbeCache.sqlite-wal") |
| 79 |
+ let archiveJournalURL = temporaryDirectory.appending(path: "HealthProbeArchive.sqlite-journal") |
|
| 76 | 80 |
try Data("archive".utf8).write(to: archiveURL)
|
| 77 | 81 |
try Data("cache".utf8).write(to: cacheWALURL)
|
| 82 |
+ try Data("journal".utf8).write(to: archiveJournalURL)
|
|
| 78 | 83 |
|
| 79 |
- PrototypeStoreResetPolicy.requestResetOnNextLaunch(defaults: defaults) |
|
| 84 |
+ PrototypeStoreResetPolicy.requestResetOnNextLaunch( |
|
| 85 |
+ defaults: defaults, |
|
| 86 |
+ appSupportURL: temporaryDirectory |
|
| 87 |
+ ) |
|
| 80 | 88 |
|
| 81 | 89 |
let result = try PrototypeStoreResetPolicy.applyIfNeeded( |
| 82 | 90 |
appSupportURL: temporaryDirectory, |
@@ -86,19 +94,66 @@ final class PrototypeStoreResetPolicyTests: XCTestCase {
|
||
| 86 | 94 |
XCTAssertTrue(result.didReset) |
| 87 | 95 |
XCTAssertEqual(Set(result.removedURLs.map(\.lastPathComponent)), Set([ |
| 88 | 96 |
"HealthProbeArchive.sqlite", |
| 97 |
+ "HealthProbeArchive.sqlite-journal", |
|
| 89 | 98 |
"HealthProbeCache.sqlite-wal" |
| 90 | 99 |
])) |
| 91 | 100 |
XCTAssertFalse(FileManager.default.fileExists(atPath: archiveURL.path)) |
| 92 | 101 |
XCTAssertFalse(FileManager.default.fileExists(atPath: cacheWALURL.path)) |
| 102 |
+ XCTAssertFalse(FileManager.default.fileExists(atPath: archiveJournalURL.path)) |
|
| 93 | 103 |
XCTAssertFalse(PrototypeStoreResetPolicy.isResetScheduled(defaults: defaults)) |
| 94 | 104 |
} |
| 95 | 105 |
|
| 96 | 106 |
func testManualResetFlagSurvivesWithoutChangingGenerationKey() throws {
|
| 97 | 107 |
defaults.set(PrototypeStoreResetPolicy.currentGeneration, forKey: PrototypeStoreResetPolicy.defaultsKey) |
| 98 | 108 |
|
| 99 |
- PrototypeStoreResetPolicy.requestResetOnNextLaunch(defaults: defaults) |
|
| 109 |
+ PrototypeStoreResetPolicy.requestResetOnNextLaunch( |
|
| 110 |
+ defaults: defaults, |
|
| 111 |
+ appSupportURL: temporaryDirectory |
|
| 112 |
+ ) |
|
| 100 | 113 |
|
| 101 | 114 |
XCTAssertEqual(defaults.integer(forKey: PrototypeStoreResetPolicy.defaultsKey), PrototypeStoreResetPolicy.currentGeneration) |
| 102 | 115 |
XCTAssertTrue(defaults.bool(forKey: PrototypeStoreResetPolicy.manualResetDefaultsKey)) |
| 103 | 116 |
} |
| 117 |
+ |
|
| 118 |
+ func testRequestResetOnNextLaunchWritesDiskMarker() throws {
|
|
| 119 |
+ defaults.set(PrototypeStoreResetPolicy.currentGeneration, forKey: PrototypeStoreResetPolicy.defaultsKey) |
|
| 120 |
+ |
|
| 121 |
+ PrototypeStoreResetPolicy.requestResetOnNextLaunch( |
|
| 122 |
+ defaults: defaults, |
|
| 123 |
+ appSupportURL: temporaryDirectory |
|
| 124 |
+ ) |
|
| 125 |
+ |
|
| 126 |
+ XCTAssertTrue(PrototypeStoreResetPolicy.isResetScheduled( |
|
| 127 |
+ defaults: defaults, |
|
| 128 |
+ appSupportURL: temporaryDirectory |
|
| 129 |
+ )) |
|
| 130 |
+ XCTAssertTrue(FileManager.default.fileExists( |
|
| 131 |
+ atPath: temporaryDirectory |
|
| 132 |
+ .appending(path: PrototypeStoreResetPolicy.manualResetMarkerFileName) |
|
| 133 |
+ .path |
|
| 134 |
+ )) |
|
| 135 |
+ } |
|
| 136 |
+ |
|
| 137 |
+ func testDiskMarkerTriggersResetWhenDefaultsFlagIsMissing() throws {
|
|
| 138 |
+ defaults.set(PrototypeStoreResetPolicy.currentGeneration, forKey: PrototypeStoreResetPolicy.defaultsKey) |
|
| 139 |
+ defaults.set(false, forKey: PrototypeStoreResetPolicy.manualResetDefaultsKey) |
|
| 140 |
+ let archiveURL = temporaryDirectory.appending(path: "HealthProbeArchive.sqlite") |
|
| 141 |
+ let markerURL = temporaryDirectory.appending(path: PrototypeStoreResetPolicy.manualResetMarkerFileName) |
|
| 142 |
+ try Data("archive".utf8).write(to: archiveURL)
|
|
| 143 |
+ try Data("pending".utf8).write(to: markerURL)
|
|
| 144 |
+ |
|
| 145 |
+ let result = try PrototypeStoreResetPolicy.applyIfNeeded( |
|
| 146 |
+ appSupportURL: temporaryDirectory, |
|
| 147 |
+ defaults: defaults |
|
| 148 |
+ ) |
|
| 149 |
+ |
|
| 150 |
+ XCTAssertTrue(result.didReset) |
|
| 151 |
+ XCTAssertEqual(result.removedURLs.map(\.lastPathComponent), ["HealthProbeArchive.sqlite"]) |
|
| 152 |
+ XCTAssertFalse(FileManager.default.fileExists(atPath: archiveURL.path)) |
|
| 153 |
+ XCTAssertFalse(FileManager.default.fileExists(atPath: markerURL.path)) |
|
| 154 |
+ XCTAssertFalse(PrototypeStoreResetPolicy.isResetScheduled( |
|
| 155 |
+ defaults: defaults, |
|
| 156 |
+ appSupportURL: temporaryDirectory |
|
| 157 |
+ )) |
|
| 158 |
+ } |
|
| 104 | 159 |
} |