Showing 2 changed files with 63 additions and 17 deletions
+1 -1
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -25,7 +25,7 @@ There are no real deployments, only test installations. Existing prototype datab
25 25
 |------|----------------|--------------------|
26 26
 | Product docs | Updated | Keep `HealthProbe/Doc/README.md` as canonical index |
27 27
 | HealthKit capture | Capture now opens one archive observation per user-visible snapshot, attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it, no longer aborts initial full-history imports after a fixed 30-minute wall-clock cap while page-level HealthKit timeouts remain in place, defers grouped observation summary/daily aggregate rebuilds until per-type verification instead of rebuilding after every imported page, and persists large HealthKit pages in smaller archive chunks while using type-specific import strategies: conservative paging for the heaviest metrics, more aggressive pages/chunks for ordinary metrics, adaptive write chunk sizing, batched deleted-object persistence, explicit task yields, and lower-allocation streaming loops to avoid long monolithic SQLite stalls | Continue moving UI/cache reads to archive-backed observation ids and revisit full checkpoint/resume and background collection separately |
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, the hot write path now reuses prepared SQLite statements within grouped page writes instead of reparsing the same SQL for every sample, caches repeated sample-type/source/source-revision/device/metadata id lookups within grouped writes, processes sample rows in a lower-allocation streaming loop, batches same-page deleted-object evidence in one transaction, adds composite indexes for visibility-range and sample-uuid hot lookups, and opens SQLite connections with import-friendly busy timeout / synchronous / temp-store pragmas | Continue moving capture/Dashboard actions to archive/cache DTOs |
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, the hot write path now reuses prepared SQLite statements within grouped page writes instead of reparsing the same SQL for every sample, caches repeated sample-type/source/source-revision/device/metadata id lookups within grouped writes, skips redundant visibility close/existence checks when grouped imports create a brand-new sample or payload version, processes sample rows in a lower-allocation streaming loop, batches same-page deleted-object evidence in one transaction, adds composite indexes for visibility-range and sample-uuid hot lookups, and opens SQLite connections with import-friendly busy timeout / synchronous / temp-store pragmas | 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 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 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 now route through DTOs and snapshot ids, with the remaining 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; Settings can now schedule a full test-database reset for the next app launch | Stop writing prototype `HealthSnapshot` bridge rows during capture/review |
+62 -16
HealthProbe/Services/SQLiteHealthArchiveStore.swift
@@ -2117,22 +2117,50 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2117 2117
             db: db,
2118 2118
             statementCache: statementCache
2119 2119
         )
2120
-        try closeOpenVisibilityRanges(
2121
-            sampleID: sampleResult.id,
2122
-            excludingVersionID: versionResult.id,
2123
-            closedAtObservationID: observationID,
2124
-            observedAt: row.observedAt,
2125
-            db: db,
2126
-            statementCache: statementCache
2127
-        )
2128
-        try insertOpenVisibilityRangeIfNeeded(
2129
-            sampleID: sampleResult.id,
2130
-            versionID: versionResult.id,
2131
-            observationID: observationID,
2132
-            observedAt: row.observedAt,
2133
-            db: db,
2134
-            statementCache: statementCache
2135
-        )
2120
+        if sampleResult.inserted {
2121
+            try insertOpenVisibilityRange(
2122
+                sampleID: sampleResult.id,
2123
+                versionID: versionResult.id,
2124
+                observationID: observationID,
2125
+                observedAt: row.observedAt,
2126
+                db: db,
2127
+                statementCache: statementCache
2128
+            )
2129
+        } else if versionResult.inserted {
2130
+            try closeOpenVisibilityRanges(
2131
+                sampleID: sampleResult.id,
2132
+                excludingVersionID: versionResult.id,
2133
+                closedAtObservationID: observationID,
2134
+                observedAt: row.observedAt,
2135
+                db: db,
2136
+                statementCache: statementCache
2137
+            )
2138
+            try insertOpenVisibilityRange(
2139
+                sampleID: sampleResult.id,
2140
+                versionID: versionResult.id,
2141
+                observationID: observationID,
2142
+                observedAt: row.observedAt,
2143
+                db: db,
2144
+                statementCache: statementCache
2145
+            )
2146
+        } else {
2147
+            try closeOpenVisibilityRanges(
2148
+                sampleID: sampleResult.id,
2149
+                excludingVersionID: versionResult.id,
2150
+                closedAtObservationID: observationID,
2151
+                observedAt: row.observedAt,
2152
+                db: db,
2153
+                statementCache: statementCache
2154
+            )
2155
+            try insertOpenVisibilityRangeIfNeeded(
2156
+                sampleID: sampleResult.id,
2157
+                versionID: versionResult.id,
2158
+                observationID: observationID,
2159
+                observedAt: row.observedAt,
2160
+                db: db,
2161
+                statementCache: statementCache
2162
+            )
2163
+        }
2136 2164
 
2137 2165
         return ArchiveV2SampleWriteResult(sampleTypeID: sampleTypeID, kind: writeKind)
2138 2166
     }
@@ -2777,6 +2805,24 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2777 2805
             bindInt64(versionID, to: 2, in: statement)
2778 2806
         }
2779 2807
         guard existing == nil else { return }
2808
+        try insertOpenVisibilityRange(
2809
+            sampleID: sampleID,
2810
+            versionID: versionID,
2811
+            observationID: observationID,
2812
+            observedAt: observedAt,
2813
+            db: db,
2814
+            statementCache: statementCache
2815
+        )
2816
+    }
2817
+
2818
+    private func insertOpenVisibilityRange(
2819
+        sampleID: Int64,
2820
+        versionID: Int64,
2821
+        observationID: Int64,
2822
+        observedAt: Date,
2823
+        db: OpaquePointer?,
2824
+        statementCache: SQLiteStatementCache? = nil
2825
+    ) throws {
2780 2826
         try withStatement(
2781 2827
             """
2782 2828
             INSERT OR IGNORE INTO sample_visibility_ranges (