diff --git a/.gitignore b/.gitignore index 02b598dfb4..af03cd5efe 100644 --- a/.gitignore +++ b/.gitignore @@ -18,12 +18,15 @@ xcuserdata/ *.xcscmblueprint *.xcuserstate .DS_Store +.claude ## Obj-C/Swift specific *.hmap *.ipa + ## Playgrounds *.playground playground.xcworkspace timeline.xctimeline + diff --git a/.gitmodules b/.gitmodules index 1308e60c03..3637ab97e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,21 +1,21 @@ [submodule "Loop"] path = Loop - url = https://github.com/LoopKit/Loop.git + url = https://github.com/loopkitdev/Loop.git [submodule "LoopKit"] path = LoopKit - url = https://github.com/LoopKit/LoopKit.git + url = https://github.com/loopkitdev/LoopKit.git [submodule "CGMBLEKit"] path = CGMBLEKit - url = https://github.com/LoopKit/CGMBLEKit.git + url = https://github.com/loopkitdev/CGMBLEKit.git [submodule "dexcom-share-client-swift"] path = dexcom-share-client-swift - url = https://github.com/LoopKit/dexcom-share-client-swift.git + url = https://github.com/loopkitdev/dexcom-share-client-swift.git [submodule "RileyLinkKit"] path = RileyLinkKit - url = https://github.com/LoopKit/RileyLinkKit + url = https://github.com/loopkitdev/RileyLinkKit [submodule "NightscoutService"] path = NightscoutService - url = https://github.com/LoopKit/NightscoutService.git + url = https://github.com/loopkitdev/NightscoutService.git [submodule "Minizip"] path = Minizip url = https://github.com/LoopKit/Minizip.git @@ -24,37 +24,37 @@ url = https://github.com/LoopKit/TrueTime.swift.git [submodule "LoopOnboarding"] path = LoopOnboarding - url = https://github.com/LoopKit/LoopOnboarding.git + url = https://github.com/loopkitdev/LoopOnboarding.git [submodule "AmplitudeService"] path = AmplitudeService - url = https://github.com/LoopKit/AmplitudeService.git + url = https://github.com/loopkitdev/AmplitudeService.git [submodule "LogglyService"] path = LogglyService - url = https://github.com/LoopKit/LogglyService.git + url = https://github.com/loopkitdev/LogglyService.git [submodule "OmniBLE"] path = OmniBLE - url = https://github.com/LoopKit/OmniBLE.git + url = https://github.com/loopkitdev/OmniBLE.git [submodule "NightscoutRemoteCGM"] path = NightscoutRemoteCGM - url = https://github.com/LoopKit/NightscoutRemoteCGM.git + url = https://github.com/loopkitdev/NightscoutRemoteCGM.git [submodule "LoopSupport"] path = LoopSupport - url = https://github.com/LoopKit/LoopSupport + url = https://github.com/loopkitdev/LoopSupport [submodule "G7SensorKit"] path = G7SensorKit - url = https://github.com/LoopKit/G7SensorKit.git + url = https://github.com/loopkitdev/G7SensorKit.git [submodule "TidepoolService"] path = TidepoolService - url = https://github.com/LoopKit/TidepoolService.git + url = https://github.com/loopkitdev/TidepoolService.git [submodule "OmniKit"] path = OmniKit - url = https://github.com/LoopKit/OmniKit.git + url = https://github.com/loopkitdev/OmniKit.git [submodule "MinimedKit"] path = MinimedKit - url = https://github.com/LoopKit/MinimedKit.git + url = https://github.com/loopkitdev/MinimedKit.git [submodule "MixpanelService"] path = MixpanelService - url = https://github.com/LoopKit/MixpanelService + url = https://github.com/loopkitdev/MixpanelService [submodule "LibreTransmitter"] path = LibreTransmitter - url = https://github.com/LoopKit/LibreTransmitter.git + url = https://github.com/loopkitdev/LibreTransmitter.git diff --git a/AmplitudeService b/AmplitudeService index fd9df8f489..d73d20d3a9 160000 --- a/AmplitudeService +++ b/AmplitudeService @@ -1 +1 @@ -Subproject commit fd9df8f48947f2cadc2a017ab88fdae074e32d96 +Subproject commit d73d20d3a9c27a200ca30f9672b6d165b2114dc9 diff --git a/CGMBLEKit b/CGMBLEKit index edd8fb232e..48bbfad581 160000 --- a/CGMBLEKit +++ b/CGMBLEKit @@ -1 +1 @@ -Subproject commit edd8fb232e18a09a6c162b489172ea9d381d7bb6 +Subproject commit 48bbfad581d918c28dde08af237edc6f730f3696 diff --git a/G7SensorKit b/G7SensorKit index 890e60754d..468eefc12c 160000 --- a/G7SensorKit +++ b/G7SensorKit @@ -1 +1 @@ -Subproject commit 890e60754ded6b1610c8b8fac7a3c026bf704a64 +Subproject commit 468eefc12c936e6d14e0582f9f3bf242b57f6096 diff --git a/LOOPKIT_SYNC_PROCESS.md b/LOOPKIT_SYNC_PROCESS.md new file mode 100644 index 0000000000..e671d54f1e --- /dev/null +++ b/LOOPKIT_SYNC_PROCESS.md @@ -0,0 +1,440 @@ +# LoopKit ↔ Tidepool Sync Process + +**Purpose:** A repeatable process an developer can follow to merge changes from Tidepool's fork of the Loop +ecosystem back into the LoopKit DIY repos, resolving conflicts with full contextual understanding. + +**Last updated:** 2026-03-10 (added Golden Rule; clarified .strings vs .xcstrings handling) + +--- + +## Background & Architecture + +The Loop ecosystem consists of two parallel development streams: + +- **LoopKit (DIY/open source):** The community-maintained fork at `github.com/LoopKit/*`. + Organized as a set of git submodules in `LoopWorkspace`. +- **Tidepool:** A company building a supported version of Loop at `github.com/tidepool-org/*`. + Tidepool's repos are **forks** of the LoopKit repos, with additional clinical/regulatory features. + +Changes flow in both directions over time, but syncing is typically done in the direction: +**tidepool-org → LoopKit**, bringing Tidepool's upstream improvements back to DIY, and then DIY -> Tidepool + +--- + +## ⭐ Golden Rule: Prefer Tidepool, Protect DIY + +**When in doubt about a conflict resolution, prefer Tidepool's version — but never at the cost of +breaking or removing DIY functionality.** + +More specifically: + +- **Tidepool changes win** for: algorithm improvements, bug fixes, new clinical features, API + changes, architecture decisions, test coverage, Swift version upgrades. +- **LoopKit DIY wins** for: anything that is *exclusively* a DIY capability — community + translations, open-source-only build paths, non-Tidepool signing/bundle IDs, features that + only exist in DIY and would be silently deleted by taking Tidepool's version. +- **Keep both** when a DIY feature and a Tidepool feature occupy the same code area but serve + different purposes and can coexist (e.g. an added Tidepool service upload alongside an existing + Nightscout upload). +- **Never silently drop** a DIY feature. If Tidepool's version removes something DIY users + depend on, document it explicitly in the per-repo sync log and flag it for human review (⚠️) + rather than quietly taking Tidepool's side. + +**Practical decision tree for any conflicting hunk:** + +1. Is this a pure algorithm/logic change? → Take Tidepool's. +2. Does Tidepool's version remove a capability DIY users have? → Keep both if possible; if not, + flag for human review. +3. Is this cosmetic (formatting, style, ordering)? → Take Tidepool's; not worth fighting over. +4. Is this a build/project setting (deployment target, signing, bundle ID)? → See the + "project.pbxproj" section below for specific rules. +5. Is this a Tidepool-only backend integration (e.g. Coastal, Tidepool upload)? → Keep it; + it doesn't harm DIY users and removing it creates future conflicts. + +--- + +### ⚠️ Key Architectural Divergence: LoopAlgorithm + +The most important structural difference between the two streams: + +- **Tidepool** extracted the core Loop algorithm into a standalone Swift Package: + `tidepool-org/LoopAlgorithm` (a fork of `LoopKit/LoopAlgorithm`). + Tidepool's `LoopKit` repo declares it as a SwiftPM dependency (in `Package.resolved`). +- **LoopKit DIY** still embeds the algorithm code inline inside `LoopKit/LoopKit/LoopAlgorithm/`. + +This means when syncing `LoopKit`: +- Algorithm changes Tidepool made via their `LoopAlgorithm` package need to be found by + looking at `tidepool-org/LoopAlgorithm` commits AND `tidepool-org/LoopKit` commits. +- Conflicts in `LoopAlgorithm.swift`, `LoopPredictionOutput.swift`, etc. inside LoopKit + may reflect changes that Tidepool now maintains in their separate package. +- **LoopAlgorithm must be synced first** (as a standalone package repo) before syncing LoopKit. + +--- + +## Repository Map + +All repos below are submodules of `LoopWorkspace`, except `LoopAlgorithm` which is standalone. + +| Repo | LoopKit branch | Tidepool fork | Notes | +|------|---------------|---------------|-------| +| **LoopAlgorithm** | `main` | `tidepool-org/LoopAlgorithm` | ⚠️ Standalone Swift Package. Sync FIRST. Not in sync.swift. | +| LoopKit | `dev` | `tidepool-org/LoopKit` | ⚠️ Complex. References LoopAlgorithm as package (Tidepool) vs inline (DIY). | +| Loop | `dev` | `tidepool-org/Loop` | Most complex. Many source conflicts. Sync LAST. | +| TidepoolService | `dev` | `tidepool-org/TidepoolService` | Tidepool-specific service; sync carefully | +| OmniBLE | `dev` | `tidepool-org/OmniBLE` | Pump driver; test after merge | +| OmniKit | `main` | `tidepool-org/OmniKit` | Pump driver; test after merge | +| MinimedKit | `main` | `tidepool-org/MinimedKit` | Pump driver; test after merge | +| NightscoutService | `dev` | `tidepool-org/NightscoutService` | Service layer | +| LibreTransmitter | `main` | `tidepool-org/LibreTransmitter` | CGM driver | +| G7SensorKit | `main` | `tidepool-org/G7SensorKit` | CGM driver | +| CGMBLEKit | `dev` | `tidepool-org/CGMBLEKit` | CGM driver | +| dexcom-share-client-swift | `dev` | `tidepool-org/dexcom-share-client-swift` | CGM client | +| RileyLinkKit | `dev` | `tidepool-org/RileyLinkKit` | Radio hardware | +| LoopOnboarding | `dev` | `tidepool-org/LoopOnboarding` | Onboarding UI | +| LoopSupport | `dev` | `tidepool-org/LoopSupport` | Support utilities | +| AmplitudeService | `dev` | `tidepool-org/AmplitudeService` | Analytics service | +| LogglyService | `dev` | `tidepool-org/LogglyService` | Logging service | +| NightscoutRemoteCGM | `dev` | `tidepool-org/NightscoutRemoteCGM` | CGM source | +| MixpanelService | `main` | `tidepool-org/MixpanelService` | Analytics service | + +**Not synced:** `Minizip`, `TrueTime.swift` (third-party libs, no Tidepool fork) + +--- + +## Recommended Sync Order + +**Core → App → Plugins (Peripheral)** + +The correct order is *not* simply "foundational to dependent" in a build-graph sense. +It is **core architectural decisions first**, so that by the time you reach the peripheral +plugins you already understand what the core changed — making those conflicts easier to +read and resolve coherently. + +In practice, a conflict in a pump driver that looks like "Tidepool changed the DoseEntry +type" only makes sense once you've already seen that LoopKit changed `DoseEntry` in the +core repo. If you do plugins first, you're resolving conflicts blind. + +### Tier 1 — Core (do these first) +1. **LoopAlgorithm** — standalone package; establishes the algorithm API everything else uses +2. **LoopKit** — foundational types (`DoseEntry`, `GlucoseValue`, `Guardrail`, etc.); all plugins depend on it + +### Tier 2 — App (do before plugins) +3. **Loop** — the top-level app; resolving this *before* plugins means you understand which + app-level API changes the plugins are expected to match, rather than discovering surprises later + +### Tier 3 — Plugins / Peripheral (do last, in any order) +4. **CGM drivers**: CGMBLEKit, G7SensorKit, dexcom-share-client-swift, NightscoutRemoteCGM, LibreTransmitter +5. **Pump drivers**: RileyLinkKit, OmniKit, OmniBLE, MinimedKit +6. **Services**: TidepoolService, NightscoutService, AmplitudeService, LogglyService, MixpanelService +7. **Support/Onboarding**: LoopSupport, LoopOnboarding + +> **Note:** pbxproj-only conflicts in peripheral repos can be batched and resolved +> mechanically at any point since they don't require architectural context. Swift source +> conflicts in peripheral repos should wait until Tier 1 + 2 are done. + +--- + +## Setup (One-Time Per Workspace Clone) + +For each repo in the sync list, add the Tidepool remote if not already present: + +```bash +cd LoopWorkspace/ +git remote add tidepool https://github.com/tidepool-org/.git +git fetch tidepool +``` + +For LoopAlgorithm (standalone — clone separately): +```bash +cd LoopWorkspace # or wherever you keep it +git clone https://github.com/LoopKit/LoopAlgorithm.git +cd LoopAlgorithm +git remote add tidepool https://github.com/tidepool-org/LoopAlgorithm.git +git fetch tidepool +``` + +--- + +## Per-Repo Sync Process + +Repeat these steps for each repo, in the order listed above. + +### Step 1 — Choose a sync branch name + +Use a consistent name across all repos for this sync run, e.g.: +``` +tidepool-sync/YYYY-MM-DD +``` + +### Step 2 — Create sync branch + +```bash +cd LoopWorkspace/ +git checkout origin/ # e.g. origin/dev or origin/main +git checkout -b tidepool-sync/YYYY-MM-DD +``` + +### Step 3 — Attempt merge + +```bash +git merge --no-edit tidepool/ +``` + +If the merge succeeds with no conflicts → commit and move to Step 9. +If there are conflicts → continue to Step 4. + +### Step 4 — Identify conflict files + +```bash +git diff --name-only --diff-filter=U +``` + +Categorize conflicts: +- **project.pbxproj** → See "Xcode Project File Conflicts" section below +- **Swift source files** → See "Source Code Conflicts" section below +- **Other** (yml, strings, etc.) → Research case by case + +### Step 5 — Research each conflict (Source Files) + +For each conflicting Swift file: + +**a) Understand what each side changed:** +```bash +MERGE_BASE=$(git merge-base HEAD tidepool/) + +# What did LoopKit change in this file since the merge base? +git log --oneline $MERGE_BASE..origin/ -- +git diff $MERGE_BASE..origin/ -- + +# What did Tidepool change? +git log --oneline $MERGE_BASE..tidepool/ -- +git diff $MERGE_BASE..tidepool/ -- +``` + +**b) Find related GitHub issues and PRs:** + +For each commit hash found above, search for it on GitHub: +- `https://github.com/LoopKit//commit/` +- Look at the PR that merged it (GitHub shows "merged via PR #NNN") +- Read the PR description and linked issues +- Also search for the LOOP-XXXX ticket numbers in commit messages at: + `https://github.com/LoopKit//issues` and + `https://github.com/tidepool-org//issues` + +**c) For LoopAlgorithm-related conflicts in LoopKit:** +- Check if the same change exists in `tidepool-org/LoopAlgorithm` +- The Tidepool version of a function in LoopKit may be forwarding to their package; + the DIY version keeps it inline. Preserve the inline version while incorporating + any algorithmic improvements from the package version. + +### Step 6 — Resolve conflicts + +**General principles:** Follow the ⭐ Golden Rule (see top of document) — prefer Tidepool's +version, but never silently remove DIY functionality. + +**For algorithm changes:** Take Tidepool's. Their test coverage is usually more thorough +and the algorithmic direction is the right one. Check the `LoopAlgorithm` sync doc to +understand if a conflict here is related to the HealthKit→LoopUnit migration. + +**For UI changes:** Take Tidepool's unless it removes a DIY-only UI path. Regulatory/clinical +UI additions from Tidepool are fine to include — they add capability without breaking DIY. + +**For Tidepool-specific features** (Tidepool Service uploads, Coastal integration, etc.): +Keep them — they don't break DIY users, and removing them creates future conflicts. + +**For DIY-only features** (e.g. community CGM integrations, Nightscout, open-source pump +drivers not supported by Tidepool): Protect these. They are the reason DIY exists. + +**Never silently drop** either side's work without a note in the sync doc. + +After resolving each file: +```bash +git add +``` + +### Step 7 — Resolve Xcode Project File Conflicts (project.pbxproj) + +The `.pbxproj` is a structured text file. Conflicts here are almost always about: +- **Object version** (LoopKit likely bumped for newer Xcode) +- **File references** (new Swift files added by either side) +- **Build settings** (deployment targets, signing, feature flags) +- **Localization** (LoopKit uses `.xcstrings`; Tidepool may still use `.strings`) + +**Approach:** +```bash +# See what LoopKit changed in the project file since merge base +git diff $MERGE_BASE..origin/ -- .xcodeproj/project.pbxproj + +# See what Tidepool changed +git diff $MERGE_BASE..tidepool/ -- .xcodeproj/project.pbxproj +``` + +Then look at the actual conflict markers in the file: +```bash +grep -n "<<<<<<\|=======\|>>>>>>>" .xcodeproj/project.pbxproj +``` + +**Common resolutions:** +- `objectVersion`: Keep LoopKit's (higher = newer Xcode format) +- `IPHONEOS_DEPLOYMENT_TARGET`: Take the *higher* of the two values (both are raising it) +- New file references added by LoopKit (e.g. `.xcstrings`): Keep them +- New file references added by Tidepool (new Swift files, mapping models, etc.): Keep them +- Tidepool's `.strings` localization file references: **Drop them** (DIY deleted these; see Pattern below) +- Tidepool's `XCRemoteSwiftPackageReference "LoopAlgorithm"`: **Omit from DIY** (DIY embeds it inline) +- LoopKit bundle IDs (`com.loopkit.*`): Keep LoopKit's +- Tidepool bundle IDs (`com.tidepool.*`): Keep Tidepool's (they're for different targets) +- Signing/provisioning settings: Keep LoopKit's for shared targets + +After resolving: +```bash +git add .xcodeproj/project.pbxproj +``` + +### Step 8 — Commit + +```bash +git commit -m "Merge tidepool/dev into tidepool-sync/YYYY-MM-DD + +Resolved conflicts: +- : +- : + +See sync-docs/.md for full context." +``` + +### Step 9 — Document in per-repo log + +Update `sync-docs/.md` with: +- Merge base commit hash +- LoopKit and Tidepool tip commit hashes +- For each resolved conflict: + - The file path + - Relevant commit hashes from each side + - Links to GitHub PRs/issues + - What each side was trying to do + - How it was resolved and why + - Any features that need testing as a result +- Any open questions or items requiring human review + +### Step 10 — Update SYNC_PROGRESS.md + +Mark the repo as done (✅) or blocked (❌) in the progress table. +Note any cross-repo dependencies discovered (e.g. "LoopKit change requires matching Loop change"). + +### Step 11 — Push and create PR (when ready) + +```bash +git push origin tidepool-sync/YYYY-MM-DD +# Then open a PR on GitHub: tidepool-sync/YYYY-MM-DD → +``` + +--- + +## Special Case: LoopAlgorithm + +LoopAlgorithm lives at `LoopKit/LoopAlgorithm` (not a submodule of LoopWorkspace) and +`tidepool-org/LoopAlgorithm` is a fork of it. + +**Key questions before syncing:** +1. Is the DIY `LoopAlgorithm` used as a package by Loop/LoopKit, or still embedded inline? + - If package: sync just like any other repo + - If inline (current DIY state): sync algorithm changes need to flow into LoopKit's + `LoopKit/LoopAlgorithm/` subdirectory AND the standalone LoopAlgorithm package + +2. What is the current pinned version of `tidepool-org/LoopAlgorithm` in Tidepool's LoopKit? + Check `LoopKit.xcodeproj/.../Package.resolved` on `tidepool/dev`. + +3. Clone LoopAlgorithm separately: + ```bash + git clone https://github.com/LoopKit/LoopAlgorithm.git + cd LoopAlgorithm + git remote add tidepool https://github.com/tidepool-org/LoopAlgorithm.git + git fetch tidepool + ``` + +4. Follow the same per-repo sync process above. +5. After resolving LoopAlgorithm, **also check** whether any of the same changes need to be + applied to the inline copy in `LoopKit/LoopKit/LoopAlgorithm/` (if DIY hasn't adopted the package yet). + +--- + +## Common Patterns to Watch For + +### Pattern: Tidepool adds LoopAlgorithm package dependency +In Tidepool's `LoopKit`, the `project.pbxproj` will have: +``` +XCRemoteSwiftPackageReference "LoopAlgorithm" +repositoryURL = "https://github.com/tidepool-org/LoopAlgorithm"; +``` +**Resolution for DIY:** Omit this package reference. Keep the inline `LoopAlgorithm/` code. +However, DO bring in any algorithmic logic changes from the inline vs. package versions. + +### Pattern: Deployment target bumps +Tidepool regularly bumps `IPHONEOS_DEPLOYMENT_TARGET`. DIY follows at its own pace. +**Resolution:** Take the higher value unless there's a specific reason not to. +Check LoopKit's own dev branch to see what they've already committed to. + +### Pattern: Localization format migration +LoopKit DIY migrated from `.strings` files to `.xcstrings` (Xcode 15+ string catalogs). +Tidepool does not maintain translations and remains on `.strings`. + +**Resolution:** Keep LoopKit's `.xcstrings` format. **Never re-add Tidepool's `.strings` +file references** — Tidepool's `.strings` references belong to files that DIY deliberately +deleted when migrating to string catalogs. Re-adding them would cause build errors +("file not found") because the actual `.strings` files no longer exist in DIY's tree. + +In practice, when resolving `project.pbxproj` conflicts: +- Drop any `PBXFileReference` entries for `*.strings` that came from Tidepool's side +- Drop any `PBXBuildFile` entries referencing those same `.strings` files +- Drop any group `children = (...)` entries pointing to `.strings` files from Tidepool +- Keep DIY's `*.xcstrings` references +- Keep all of Tidepool's non-translation additions (new Swift files, mapping models, etc.) + +### Pattern: Tidepool-specific regulatory features +Features like Coastal integration, FDA submission mode, specific clinical guardrails. +**Resolution:** Keep them. They add capability without breaking DIY. Only omit if they +require Tidepool backend infrastructure that simply won't exist in DIY. + +### Pattern: Bundle identifier differences +`com.loopkit.*` (LoopKit) vs `com.tidepool.*` (Tidepool). +**Resolution:** Keep both — they apply to different build targets/schemes. + +### Pattern: HKUnit.swift removal +Both sides removed the `HKUnit.swift` extension file (HealthKit unit helpers were moved). +This should auto-merge or be a trivially clean conflict. If not, take the removal. + +--- + +## Testing Checklist After Merge + +After completing all repos, test these critical paths before opening PRs: + +- [ ] **Glucose display:** CGM data flows and displays correctly +- [ ] **Insulin delivery:** Bolus and basal commands work (OmniBLE/OmniKit/Minimed) +- [ ] **Loop algorithm:** Closed loop prediction and dosing recommendations +- [ ] **Remote services:** Nightscout upload/download, Tidepool service +- [ ] **Onboarding:** Fresh install and therapy settings configuration +- [ ] **Watch app:** Complication and status display (if applicable) +- [ ] **Widgets:** Lock screen / home screen widgets (if applicable) +- [ ] **Build:** All targets compile cleanly with no warnings promoted to errors + +--- + +## Tracking Files + +| File | Purpose | +|------|---------| +| `SYNC_PROGRESS.md` | Master status table, notes on blocked items | +| `sync-docs/.md` | Per-repo conflict log with full context and links | +| `LOOPKIT_SYNC_PROCESS.md` | This file — the process itself | + +--- + +## Reference Links + +- LoopKit org: https://github.com/LoopKit +- Tidepool org: https://github.com/tidepool-org +- LoopAlgorithm (LoopKit): https://github.com/LoopKit/LoopAlgorithm +- LoopAlgorithm (Tidepool): https://github.com/tidepool-org/LoopAlgorithm +- LoopWorkspace: https://github.com/LoopKit/LoopWorkspace +- Original sync script: `LoopWorkspace/Scripts/sync.swift` diff --git a/LibreTransmitter b/LibreTransmitter index d0d301208f..356824bfd8 160000 --- a/LibreTransmitter +++ b/LibreTransmitter @@ -1 +1 @@ -Subproject commit d0d301208faeb2bc763454baf0550f3fd4888bb7 +Subproject commit 356824bfd8482dbd54e12a8553c459a698d48ede diff --git a/LogglyService b/LogglyService index d6df99ea34..44f97c9977 160000 --- a/LogglyService +++ b/LogglyService @@ -1 +1 @@ -Subproject commit d6df99ea34658c42eb721829d29812645c08fdad +Subproject commit 44f97c99779ba04154f541e4527e512f092f9120 diff --git a/Loop b/Loop index 40ae514ef2..82193f41bf 160000 --- a/Loop +++ b/Loop @@ -1 +1 @@ -Subproject commit 40ae514ef2cb6ee8cf0a62177de3072a460ee2e4 +Subproject commit 82193f41bf61a41a749c6bb8c8ef65d386bb2bae diff --git a/LoopKit b/LoopKit index e7e2ee2b54..c17ef4b4ea 160000 --- a/LoopKit +++ b/LoopKit @@ -1 +1 @@ -Subproject commit e7e2ee2b546c4d8122014838cb98a0e26dd91208 +Subproject commit c17ef4b4ea5e3c8d8e57db4ffa310c06364d5cec diff --git a/LoopOnboarding b/LoopOnboarding index 64f978e143..da95d69d2c 160000 --- a/LoopOnboarding +++ b/LoopOnboarding @@ -1 +1 @@ -Subproject commit 64f978e143723765452957cef06a99db380b128c +Subproject commit da95d69d2c3f35861066dbe704fadb4e4117e787 diff --git a/LoopSupport b/LoopSupport index 0c296289ed..b93873f4f4 160000 --- a/LoopSupport +++ b/LoopSupport @@ -1 +1 @@ -Subproject commit 0c296289ed8698cbc3acd4c1ea1b39a600c0dbc3 +Subproject commit b93873f4f49458d5c23de6a0b98cb7893958d480 diff --git a/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved index 85b387d04a..11664817cf 100644 --- a/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7645108625333b4ec60e0e439db0c0dc8a91ad0942d36797c6b66208a9082ea2", + "originHash" : "9fa433ef5fce7eff885b44f4dea36e033d61f148853051ee0494bf4e79200676", "pins" : [ { "identity" : "amplitude-ios", @@ -64,6 +64,15 @@ "version" : "1.7.1" } }, + { + "identity" : "json-logic-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/advantagefse/json-logic-swift", + "state" : { + "revision" : "9088eed1b26937fe13d248aa24d7632a51be28e2", + "version" : "1.2.4" + } + }, { "identity" : "kituracontracts", "kind" : "remoteSourceControl", @@ -82,13 +91,22 @@ "version" : "2.0.0" } }, + { + "identity" : "loopalgorithm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/LoopKit/LoopAlgorithm", + "state" : { + "branch" : "main", + "revision" : "1ca85662b1f8799758988108f46dba5f34d4889b" + } + }, { "identity" : "mixpanel-swift", "kind" : "remoteSourceControl", "location" : "https://github.com/mixpanel/mixpanel-swift.git", "state" : { "branch" : "master", - "revision" : "c676a9737c76e127e3ae5776247b226bc6d7652d" + "revision" : "b4cb3f6e3e3084e1637c4dfe06c2dcda169ff523" } }, { @@ -159,7 +177,7 @@ "location" : "https://github.com/tidepool-org/TidepoolKit", "state" : { "branch" : "dev", - "revision" : "54045c2e7d720dcd8a0909037772dcd6f54f0158" + "revision" : "4f4747ff647d836c5a27cc1b9c275e5717901e83" } }, { @@ -167,7 +185,6 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/LoopKit/ZIPFoundation.git", "state" : { - "branch" : "stream-entry", "revision" : "c67b7509ec82ee2b4b0ab3f97742b94ed9692494" } } diff --git a/MinimedKit b/MinimedKit index 106467e8f8..f19f99cc0b 160000 --- a/MinimedKit +++ b/MinimedKit @@ -1 +1 @@ -Subproject commit 106467e8f8effeae5a2872d121a33b548350f25c +Subproject commit f19f99cc0b552fc6996412285f9a1e450a4f4961 diff --git a/MixpanelService b/MixpanelService index b33debdac3..4a4b433c71 160000 --- a/MixpanelService +++ b/MixpanelService @@ -1 +1 @@ -Subproject commit b33debdac37d6ef3be955eebb0c42495a1f19232 +Subproject commit 4a4b433c7181fd8d6fa7d22840fd8466a3f6101e diff --git a/NightscoutRemoteCGM b/NightscoutRemoteCGM index 383d3c1e6b..00eefaa056 160000 --- a/NightscoutRemoteCGM +++ b/NightscoutRemoteCGM @@ -1 +1 @@ -Subproject commit 383d3c1e6b7c0c79def98a1633e4a5856bf221a4 +Subproject commit 00eefaa0561cee23ed56cb2585d8415b316ec5f1 diff --git a/NightscoutService b/NightscoutService index 7721a8da0d..09dc797b9a 160000 --- a/NightscoutService +++ b/NightscoutService @@ -1 +1 @@ -Subproject commit 7721a8da0de4f69fbc6994bdaa5c860ba9a99ede +Subproject commit 09dc797b9a30682d02f20a985fec3efb79975e14 diff --git a/OmniBLE b/OmniBLE index 4e212a81aa..87606c4b88 160000 --- a/OmniBLE +++ b/OmniBLE @@ -1 +1 @@ -Subproject commit 4e212a81aa30e3aedeb04cec6644c39463f9db8b +Subproject commit 87606c4b88e25cf3f991c5561645a434838d2054 diff --git a/OmniKit b/OmniKit index 2b4253b9fd..e5fad99f65 160000 --- a/OmniKit +++ b/OmniKit @@ -1 +1 @@ -Subproject commit 2b4253b9fd3ec167d8a6b198dae6b59606058808 +Subproject commit e5fad99f65f9c2a80425926715e97eba526871f1 diff --git a/RileyLinkKit b/RileyLinkKit index d953e1c79b..8a3164bfef 160000 --- a/RileyLinkKit +++ b/RileyLinkKit @@ -1 +1 @@ -Subproject commit d953e1c79b36f06d68b7255bb8f4331d906cc30d +Subproject commit 8a3164bfefeb749cff08acefb97e741467e566fe diff --git a/SYNC_PROGRESS.md b/SYNC_PROGRESS.md new file mode 100644 index 0000000000..07efac3738 --- /dev/null +++ b/SYNC_PROGRESS.md @@ -0,0 +1,78 @@ +# Tidepool → LoopKit Sync Progress + +**Sync branch:** `tidepool-sync/2026-05-11` +**Source:** `tidepool-org//dev` (or `main`) +**Target:** `LoopKit//dev` (or `main`) +**Started:** 2026-05-11 +**Last updated:** 2026-05-11 + +**Process doc:** [LOOPKIT_SYNC_PROCESS.md](LOOPKIT_SYNC_PROCESS.md) +**Sync log:** [docs/tidepool-sync-2026-05-11.md](docs/tidepool-sync-2026-05-11.md) +**Previous sync:** [SYNC_PROGRESS history for 2026-03-10](#) — branch merged to dev across all 18 repos + +--- + +## Status: ✅ ALL REPOS MERGED (build verification pending) + +17 repos merged, 1 noop (MixpanelService already current). All conflicts resolved; build verification in progress. + +--- + +## Full Repo Status + +| # | Repo | Base | Tidepool commits | Conflicts | Submodule commit | Status | +|---|------|------|------------------|-----------|------------------|--------| +| — | **LoopAlgorithm** | `main` | 4 (test-only) | — (pin bump) | pin → `bd1a879` | ✅ Done | +| 1 | **LoopKit** | `dev` | 409 | 18 source + 19 pbxproj | `bd30c463` | ✅ Done | +| 2 | **Loop** | `dev` | 14 | 0 source + 3 pbxproj | `76b6b1e3` | ✅ Done | +| 3 | **CGMBLEKit** | `dev` | 13 | pbxproj only (4) | `69562e7` | ✅ Done | +| 4 | **G7SensorKit** | `main` | 15 | pbxproj only (2) | `d024513` | ✅ Done | +| 5 | **dexcom-share-client-swift** | `dev` | 15 | pbxproj only (3, + orphan cleanup) | `541de2f` | ✅ Done | +| 6 | **NightscoutRemoteCGM** | `dev` | 13 | pbxproj only (2) | `b1ea9ee` | ✅ Done | +| 7 | **LibreTransmitter** | `main` | 14 | pbxproj only (3) | `c99daf1` | ✅ Done | +| 8 | **RileyLinkKit** | `dev` | 3 | pbxproj only (2) | `19f5ae8` | ✅ Done | +| 9 | **OmniKit** | `main` | 18 | OmnipodPumpManager (6 regions) + pbxproj | `b3b6080` | ✅ Done | +| 10 | **OmniBLE** | `dev` | 20 | OmniBLEPumpManager (8 regions) + pbxproj | `645e0fc` | ✅ Done | +| 11 | **MinimedKit** | `main` | 18 | MinimedPumpManager + 1 trivial + pbxproj | `f994d6e` | ✅ Done | +| 12 | **TidepoolService** | `dev` | 45 | DoseEntry + pbxproj | `5f6a064` | ✅ Done | +| 13 | **NightscoutService** | `dev` | 22 | NightscoutService + pbxproj | `1b5cded` | ✅ Done | +| 14 | **AmplitudeService** | `dev` | 6 | pbxproj only (2) | `77dae3e` | ✅ Done | +| 15 | **LogglyService** | `dev` | 6 | pbxproj only (2) | `8e18081` | ✅ Done | +| 16 | **LoopSupport** | `dev` | 11 | pbxproj only (2) | `a312dfb` | ✅ Done | +| 17 | **LoopOnboarding** | `dev` | 28 | pbxproj only (2) | `fd7e410` | ✅ Done | +| — | **MixpanelService** | `main` | 0 | — | unchanged | ✅ Noop | + +LoopWorkspace superproject commit: `3d4432c` ("Bump submodule pins to tidepool-sync/2026-05-11 heads"). + +--- + +## Next Steps + +1. **Compile test** — `xcodebuild build -workspace LoopWorkspace.xcworkspace -scheme Loop -destination 'platform=iOS Simulator,name=iPhone 17'` +2. **Fix any compile errors** that surface +3. **Push branches** to `loopkitdev/` +4. **Open PRs** — one per repo, `tidepool-sync/2026-05-11` → `dev` (or `main`) + +--- + +## DIY Divergences Established This Sync + +| Decision | Detail | +|---|---| +| `BasalRateScheduleEditor` enforces max basal | DIY rejects tidepool/LoopKit PR #734 (LOOP-5767). Keep `maximumBasalRate: therapySettings.maximumBasalRatePerHour`, not `nil`. See [`memory/divergence_basal_max_filter.md`](../memory/divergence_basal_max_filter.md). The DIY user-reported bug (max basal not respected on OmniBLE/Dash) is what surfaced this. | +| OmniBLE/OmniKit reentrant-lock fix | DIY's `isSignalLost(at:lastPumpDataReportDate:)` signature is preserved over Tidepool's `isSignalLost(at: Date = Date())` to avoid reentrant lock crashes under rapid status polling. | +| OmniBLE Pod Keep Alive suspend special case | DIY's `slot6SuspendTimeExpired` skip-ack guard preserved during the migration to `mutateState`. | +| MinimedKit CAGE/IAGE | DIY's `updateLastEventDates(from:)` for cannula and insulin age tracking preserved; Tidepool has no equivalent. | +| NightscoutService APNS response feature | DIY's `RemoteNotificationResponseManager` + JWT-managed return notifications preserved; Tidepool's simpler version dropped. | +| OmniBLE temp basal error handling | Kept DIY's `completion(.communication(error))` style; did not adopt Tidepool's `do { ... } catch` refactor because it cannot be cleanly applied to the conflict region alone. decisionId tracking already present in DIY. | +| CachedInsulinDeliveryObject bolus-without-units | Dropped the `assertionFailure` in `CachedInsulinDeliveryObject.dose` for a `.bolus` with neither programmedUnits nor deliveredUnits — legacy rows from an upgraded DIY install can have neither and trapped debug builds on read. Falls back to 0 (release behavior). Upstream keeps the assertion (fresh installs only); re-remove on future LoopKit syncs. | + +--- + +## Key Patterns Carried Forward from 2026-03-10 + +| Decision | Detail | +|---|---| +| LoopAlgorithm as Swift Package | DIY pulls `tidepool-org/LoopAlgorithm` via workspace `Package.resolved`. Do not re-add `XCRemoteSwiftPackageReference "LoopAlgorithm"` to per-repo `.pbxproj` files. | +| `.xcstrings` over `.strings` | DIY uses Xcode 15+ string catalogs; always drop Tidepool's `.strings` PBXFileReferences and variant-group children during pbxproj conflict resolution. | +| Preserve `LOCALIZATION_PREFERS_STRING_CATALOGS = YES` | Always keep DIY side of this XCBuildConfiguration setting. | diff --git a/TidepoolService b/TidepoolService index 4ef78bf8b5..765bcb9b33 160000 --- a/TidepoolService +++ b/TidepoolService @@ -1 +1 @@ -Subproject commit 4ef78bf8b58e2cee3e7f00fe7670fc2a7b166874 +Subproject commit 765bcb9b339a1c058428707d6dd8ddb0967e90a5 diff --git a/VersionOverride.xcconfig b/VersionOverride.xcconfig index b176650a02..e7d27a9033 100644 --- a/VersionOverride.xcconfig +++ b/VersionOverride.xcconfig @@ -8,5 +8,5 @@ // Version [for DIY Loop] // configure the version number in LoopWorkspace -LOOP_MARKETING_VERSION = 3.14.0 +LOOP_MARKETING_VERSION = 3.15.0 CURRENT_PROJECT_VERSION = 57 diff --git a/dexcom-share-client-swift b/dexcom-share-client-swift index 04804892ea..e4c2796377 160000 --- a/dexcom-share-client-swift +++ b/dexcom-share-client-swift @@ -1 +1 @@ -Subproject commit 04804892ea58778472e19c738ae39a87f41c0070 +Subproject commit e4c2796377c7e32c8983006f95b23c3114e6a06f diff --git a/docs/tidepool-sync-2026-03-10.md b/docs/tidepool-sync-2026-03-10.md new file mode 100644 index 0000000000..20a98e46c1 --- /dev/null +++ b/docs/tidepool-sync-2026-03-10.md @@ -0,0 +1,722 @@ +# Tidepool → LoopKit DIY Sync — 2026-03-10 + +**Branch:** `tidepool-sync/2026-03-10` (all 18 repos) +**Build status:** ✅ Compiles clean (verified 2026-03-12) + +This document describes the changes introduced by syncing the Tidepool fork of Loop +back into DIY, the conflicts encountered during that merge, and the +decisions made to resolve them. + +> **Update (2026-05-20) — read before relying on the LoopAlgorithm decisions below.** +> A follow-up sync landed on 2026-05-11; see +> [`tidepool-sync-2026-05-11.md`](tidepool-sync-2026-05-11.md) for that round. +> +> One major decision recorded in this doc has since been **reversed**: §3 states that +> DIY keeps LoopAlgorithm embedded *inline* inside LoopKit and *omits* the +> `XCRemoteSwiftPackageReference "LoopAlgorithm"` (see "TidepoolService: import +> LoopAlgorithm — Removed" and the pbxproj rules table). As of the 2026-05-11 sync, DIY +> instead consumes the **`tidepool-org/LoopAlgorithm` Swift package** directly — pinned +> in the workspace `Package.resolved` — and the inline `LoopKit/LoopAlgorithm/` copies +> (`LoopAlgorithm.swift`, `LoopPredictionOutput.swift`, `ExponentialInsulinModel.swift`) +> were deleted (LOOP-4781). `import LoopAlgorithm` is therefore present again where this +> doc says it was removed. Where the two docs disagree on LoopAlgorithm packaging, the +> 2026-05-11 doc is authoritative. + +--- + +## Table of Contents + +1. [New Features from Tidepool](#1-new-features-from-tidepool) +2. [Conflicts Encountered](#2-conflicts-encountered) +3. [Conflict Resolution Decisions](#3-conflict-resolution-decisions) +4. [DIY Features Preserved](#4-diy-features-preserved) +5. [Post-Merge Fixes](#5-post-merge-fixes) +6. [Testing Notes](#6-testing-notes) + +--- + +## 1. New Features from Tidepool + +### LoopAlgorithm — A New Architecture for DIY + +#### Background: What Changed and Why + +DIY Loop previously embedded algorithm logic directly +inside `LoopKit` as an inline copy (`LoopKit/LoopKit/LoopAlgorithm/`), and effects +calculations were distributed across the data store layer — `CarbStore` computed carb +effects, `DoseStore` computed insulin effects, and `GlucoseStore` computed glucose momentum +and retrospective correction. `LoopDataManager` orchestrated these by calling each store +asynchronously and stitching the results together. The central `LoopAlgorithm` type was +a Swift `actor` — a stateful, async object. + +This sync introduces the **LoopAlgorithm Swift Package** as an actual dependency, replacing +that architecture with something fundamentally different. + +#### The New Design: Functional and Stateless + +The package reconceives the algorithm as a **pure function**: given a complete snapshot of +the user's current state, produce a complete output. No stores, no CoreData, no HealthKit, +no async, no side effects. + +``` +AlgorithmOutput = LoopAlgorithm.run(input: AlgorithmInput) +``` + +`LoopAlgorithm` is now declared as a `caseless enum` — it cannot be instantiated. All +methods are static. There is no mutable state. The same input will always produce the +same output. + +**Input (`AlgorithmInput` protocol)** is a value-type snapshot containing everything the +algorithm needs: + +| Field | Description | +|-------|-------------| +| `glucoseHistory` | Recent CGM readings | +| `doses` | Insulin delivery history (basal + boluses) | +| `carbEntries` | Active carb entries | +| `basal` | Scheduled basal rate timeline | +| `sensitivity` | ISF timeline | +| `carbRatio` | ICR timeline | +| `target` | Glucose target range timeline | +| `suspendThreshold` | Low glucose suspend threshold | +| `maxBolus` | Safety limit | +| `maxBasalRate` | Safety limit | +| `maxActiveInsulinMultiplier` | Max IOB cap as multiple of maxBolus (default 2×) | +| `carbAbsorptionModel` | Parabolic, linear, or piecewise linear | +| `recommendationType` | Temp basal, automatic bolus, or manual | +| `useIntegralRetrospectiveCorrection` | IRC vs standard RC | +| `gradualTransitionsThreshold` | Threshold for gradual effect ramping | + +**Output (`AlgorithmOutput`)** is a complete value type containing everything produced: + +| Field | Description | +|-------|-------------| +| `recommendationResult` | `Result` — temp basal and/or bolus | +| `predictedGlucose` | Full glucose prediction curve | +| `effects` | Broken-out insulin, carb, momentum, RC, counteraction effects | +| `dosesRelativeToBasal` | Each dose expressed relative to scheduled basal | +| `activeInsulin` | Current IOB | +| `activeCarbs` | Current COB | + +The `effects` field (`LoopAlgorithmEffects`) exposes every intermediate calculation — +insulin effect curve, carb effect curve, carb absorption status per entry, retrospective +correction, glucose momentum, insulin counteraction velocities, and retrospective glucose +discrepancies. Previously these intermediate values were scattered across store callbacks +and ephemeral local variables inside `LoopDataManager`. + +#### Why This Matters for DIY + +**Testability.** Because the algorithm is a pure function over value types, tests require +no mocks, no CoreData stack, no async machinery. A test is just: + +```swift +let input = AlgorithmInputFixture(...) // or decoded from JSON +let output = LoopAlgorithm.run(input: input) +XCTAssertEqual(output.predictedGlucose, expected) +``` + +Fixtures can be serialized to and from JSON, making it trivial to capture a real-world +scenario and turn it into a regression test. The package ships with JSON fixture files +for glucose math, carb math, and dosing scenarios. + +**Portability.** Removing HealthKit and CoreData as dependencies means the algorithm can +run anywhere Swift runs — on a server, in a command-line tool, in a simulation, or in +tests — without needing a full iOS stack underneath it. + +**Clarity.** Previously, understanding what went into a dosing decision required tracing +through multiple async callbacks across multiple store classes. Now the entire decision +lives in one call with explicit inputs and outputs. + +#### What the Package Replaced vs. What Was Already There + +| Aspect | Before (DIY inline) | After (LoopAlgorithm package) | +|--------|--------------------|-----------------------------| +| `LoopAlgorithm` type | `public actor` (stateful, async) | `public enum` (no state, static methods only) | +| Entry point | `generatePrediction(input:startDate:) throws` | `run(input:) -> AlgorithmOutput` (non-throwing; errors in Result) | +| Effects calculation | Distributed across `CarbStore`, `DoseStore`, `GlucoseStore` | All in `LoopAlgorithm.generatePrediction(...)` static methods | +| Error handling | `throws` propagated through async chain | `Result<_, Error>` in output struct | +| Effects exposed | 5 fields (insulin, carbs, RC, momentum, counteraction) | 8 fields (adds carbStatus, retrospective discrepancies, total RC magnitude) | +| Error cases | 2 (`missingGlucose`, `incompleteSchedules`) | 7 (granular: glucose too old, incomplete timeline, sensitivity window, etc.) | +| Unit types | `HKUnit` / `HKQuantity` (HealthKit) | `LoopUnit` / `LoopQuantity` (custom, no HealthKit import) | +| Dependencies | HealthKit, CoreData | None (pure Swift) | +| Testability | Required mocked async stores | Pure value types; JSON fixture files included | + +#### Specific Changes in This Sync's LoopAlgorithm Version + +On top of the architectural shift, the version of LoopAlgorithm synced here includes +several algorithmic and API improvements made by Tidepool since the package was established: + +| Change | Details | +|--------|---------| +| **Gradual effect transitions** | Insulin and carb effects ramp up gradually at the start of absorption rather than stepping immediately to full effect, producing smoother prediction curves (PR #23) | +| **Configurable max active insulin multiplier** | `maxActiveInsulinMultiplier` on `AlgorithmInput` caps max IOB as a multiple of `maxBolus`; defaults to 2× (LOOP-5502, PR #22) | +| **Carb absorption model selection** | `carbAbsorptionModel` field on input allows runtime selection of parabolic, linear, or piecewise-linear absorption modeling (PR #21) | +| **AutomaticDoseRecommendation backward decode fix** | Decoding old stored recommendations without `basalAdjustment` field no longer crashes (PR #20) | +| **ISF fix during mid-absorption** | Corrects which ISF value is used during active carb absorption for more accurate corrections (PR #19) | +| **TempBasalRecommendation direction** | `.direction` field (increase / decrease / unchanged) added to `TempBasalRecommendation`; enables directional UI feedback (LOOP-5295, PR #18) | +| **Swift 6 Sendable conformances** | `AbsoluteScheduleValue` and other types marked `Sendable` throughout (PR #14) | +| **Glucose math tests moved here** | `GlucoseMathTests.swift` and JSON fixtures moved from LoopKit into this package where they belong (PR #24) | + +### Loop (App) — Presets Overhaul + +Presets have been substantially redesigned. The changes span the data model, safety +guardrails, manager architecture, UI, and Watch app. This is the most user-visible +change in this sync. + +#### Before: Override Presets in DIY Loop + +In prior DIY Loop, override presets were simple `TemporaryScheduleOverridePreset` objects +stored in `LoopSettings.overridePresets`. Each preset had a name, symbol, correction range, +insulin sensitivity multiplier, and duration. There was no preset type system — all presets +were generic overrides. Users could set a target range and/or an ISF multiplier, activate +a preset from the toolbar, and it would run until it expired or was manually cancelled. + +DIY did have a limited scheduling concept: the UIKit `AddEditOverrideTableViewController` +exposed a **Start Time** row that allowed setting a future start time for an override. +If `startDate > Date()`, the override was queued as a one-shot future event and shown in +a "SCHEDULED PRESET" section in `OverrideSelectionViewController`. There was no +day-of-week recurrence, no system alert to prompt the user at the scheduled time, and +no persistence of the schedule on the preset definition itself — the delay was set +per-activation, not stored on the preset. + +There were no guardrails specific to presets, no required training, and no safety +mitigations for extreme settings. + +#### After: SelectablePreset Type System + +Presets are now modeled as a typed enum, `SelectablePreset`, with three distinct cases: + +```swift +enum SelectablePreset { + case preMeal(range: ClosedRange) + case activity(ActivityPreset) + case custom(TemporaryPreset) +} +``` + +Each case has different capabilities enforced at the type level: + +| Capability | Pre-Meal | Activity | Custom | +|-----------|---------|----------|--------| +| Adjust correction range | ✅ | ✅ | ✅ | +| Adjust insulin needs (ISF multiplier) | ❌ | ✅ | ✅ | +| Set duration | ❌ (ends when carbs entered) | ✅ | ✅ | +| Indefinite duration | ❌ | ❌ | ✅ | +| Rename | ❌ | ❌ | ✅ | +| Schedule (day/time) | ❌ | ✅ | ✅ | +| Delete | ❌ | ❌ | ✅ | + +#### Activity Presets — Evidence-Based Defaults + +A new `.activity` case provides four pre-defined activity presets with evidence-based +default insulin reduction values. These appear in the Presets list for all users and +can be customized but not deleted: + +| Activity | Default Target Range | Default Insulin Needs | +|----------|--------------------|-----------------------| +| Jogging | 150–170 mg/dL | 21% of normal | +| Biking | 150–170 mg/dL | 23% of normal | +| Walking | 150–170 mg/dL | 23% of normal | +| Strength Training | 150–170 mg/dL | 37% of normal | + +The `insulinNeedsScaleFactor` (e.g. 0.21 for jogging) scales all three delivery parameters +simultaneously — basal rate, carb ratio, and ISF — so that "need 21% of normal insulin" +is expressed as a single unified control rather than three separate adjustments. + +When a user modifies an activity preset from its defaults, a "modified" indicator is shown +in the UI (`isModifiedFromDefault`). + +#### New Safety Feature: High Insulin Needs Mitigation (LOOP-5439) + +When a preset's `insulinNeedsScaleFactor` exceeds 165% (the upper recommended guardrail +bound), a safety mitigation is automatically applied: the effective correction range is +clamped to a minimum of **110 mg/dL**, regardless of what the user set. + +```swift +// TemporaryScheduleOverride.effectiveCorrectionRangeDuring(scheduledRange:) +if veryHighInsulinNeeds { + return range.clampedTo(atLeast: highInsulinNeedsMitigationCorrectionRangeLimit) // 110 mg/dL +} +``` + +This prevents the dangerous combination of very high insulin delivery AND a very low +correction target. Even if a user sets a correction range of 80 mg/dL while also +setting insulin needs to 180%, the system will use 110 mg/dL as the effective floor. +The UI makes this behavior visible to the user. + +#### Preset Guardrails + +| Setting | Absolute Bounds | Warning | Recommended | +|---------|----------------|---------|-------------| +| Insulin Needs | 15%–200% | 15%–190% | 15%–165% | +| Custom preset correction range | Suspend threshold–250 mg/dL | Suspend threshold–180 mg/dL | — | +| Pre-meal correction range | Suspend threshold–130 mg/dL | Dynamic (based on scheduled range) | — | + +The pre-meal guardrail is dynamic: the recommended upper bound is capped at the lower +bound of the user's current correction range schedule, encouraging a pre-meal target +that is meaningfully lower than their normal target. Pre-meal maximum is hard-capped +at 130 mg/dL absolute. + +Guardrail violations are surfaced inline during editing with color-coded warnings +(yellow = outside recommended, red = outside absolute) and a `GuardrailWarning` view +shown before saving. + +#### TemporaryPresetsManager — Separated from LoopDataManager + +Preset lifecycle management has been extracted from `LoopDataManager` into a dedicated +`TemporaryPresetsManager` (`@MainActor @Observable`). It owns: + +- The active `scheduleOverride` (with `didSet` observers for activation/deactivation events) +- `presetHistory` (a `TemporaryScheduleOverrideHistory` for IOB/ISF history reconstruction) +- Override intent observer (Siri shortcut handling) +- Preset scheduling and reminder alerts +- `basalRateScheduleApplyingOverrideHistory`, `insulinSensitivityScheduleApplyingOverrideHistory`, + `carbRatioScheduleApplyingOverrideHistory` — used by the algorithm to reconstruct what + the effective schedules were over the past few hours, accounting for any presets that were + active during that window + +#### Safety Alert: Indefinite Preset Reminder + +When a custom preset is started with indefinite duration, a repeating alert fires every +24 hours reminding the user it is still active: + +> *"[Preset Name] has been active for more than 24 hours. Make sure you still want it +> enabled, or turn it off."* + +This alert is time-sensitive (interrupts Focus modes) and repeats until the preset is +deactivated. When the preset is deactivated, the alert is retracted. + +#### Preset Scheduling — Significantly Expanded + +DIY Loop already had a basic scheduling concept: a **one-shot future start time** +that could be set per-activation in the old UIKit override editor. That delayed the +activation of an override to a future time, but the schedule was not stored on the +preset, could not repeat, and generated no alert to remind the user. + +The new scheduling system stores the schedule on the preset definition itself and adds +full day-of-week recurrence. Presets can now be scheduled to start at a specific time, +optionally repeating on selected days of the week (`PresetScheduleRepeatOptions`: +Sunday through Saturday as an `OptionSet`). When a scheduled preset's start time +approaches, a time-sensitive system alert fires asking the user whether to start it: + +> *"Would you like to start your [Preset Name] preset? This will end any active preset."* + +The alert offers "Don't Start" (dismiss) and "Yes, Start Now" (activates the preset). +The schedule is re-armed after each activation to fire again at the next scheduled time. + +| Aspect | Old DIY | New (Tidepool) | +|--------|---------|----------------| +| Schedule stored on preset | ❌ (set per-activation) | ✅ | +| One-shot future start | ✅ | ✅ | +| Day-of-week recurrence | ❌ | ✅ (any combination of days) | +| System alert at scheduled time | ❌ | ✅ (time-sensitive) | +| User confirm/dismiss in alert | ❌ | ✅ | + +#### Required Training Before Creating Presets + +New users must complete a required in-app training sequence (`PresetsTraining`) before +they can create custom presets. The training is gated by `PresetsTrainingCompletion`, +which tracks progress through 5 chapters persisted in `UserDefaults`: + +1. **Customizing Presets** — How overall insulin %, correction range, and duration work +2. **Illness** — How to use presets when sick (illness typically increases insulin needs) +3. **Daily Activities** — How to use presets for non-exercise activities +4. **Exercise** — How to use presets for exercise (aerobic vs. anaerobic differences) +5. **Training Complete** — Summary + +If a user attempts to create a new preset without completing training, an alert blocks +the action and offers to start the training flow. The training can be reviewed at any +time from the Presets screen. (Debug builds allow skipping chapters via `allowDebugFeatures`.) + +#### Presets UI + +The Presets screen (`PresetsView`) is now a dedicated full-screen view accessible from +Settings, with: + +- **Active preset card** — shown at the top when a preset is running, with expected end + time and tap-to-manage +- **All Presets list** — sortable by name, last used, or date created (ascending/descending) +- **Training card** — shown until training is complete; blocks the "+" create button +- **Presets Performance History** — a history view showing past preset activations +- **Review Training** option in the Support section + +Individual preset cards (`PresetCard`) display the preset's icon, name, duration, insulin +needs, and correction range, with guardrail-aware color coding. + +#### Other Loop (App) Changes + +| Feature | Details | +|---------|---------| +| **Active Preset Banner on CarbEntryView** | Shows the active override preset while entering carbs (LOOP-5432) | +| **DeeplinkView widget component** | Replaces `SystemActionLink.swift`; cleaner implementation of widget deep-link action buttons | +| **roundBasalRate utility** | `DeviceDataManager.roundBasalRate(unitsPerHour:)` added for consistent basal rate rounding (LOOP-5558) | +| **schedulePresets storage** | `NSUserDefaults` now persists schedule presets (LOOP-4754) | +| **defaultEnvironment key** | New `NSUserDefaults` key for Tidepool service environment selection (LOOP-5153) | +| **Granular alert permission warnings** | `AlertPermissionsChecker` (existing since 2021) updated: single "safety notifications off" alert replaced with 5 granular cases distinguishing `notificationsDisabled`, `criticalAlertsDisabled`, `timeSensitiveDisabled`, and combinations thereof — each with tailored messaging | +| **iOS 17+ onChange API** | `onChange(of:)` calls updated to the two-parameter form throughout views | +| **XCTest environment guard** | `AppDelegate` skips full initialization when running under XCTest | +| **Async/await in RemoteDataServicesManager** | `uploadCgmEventData` migrated from callback-based to `async/await Task` pattern | +| **"Save as Favorite" conditional display** | CarbEntryView only shows "Save as favorite" when no existing favorite food is selected | +| **GeometryReader removed from BolusEntryView** | Cleaner layout; fixes safe area issues | + +### Pump Drivers (OmniKit / OmniBLE / MinimedKit) + +| Feature | Details | +|---------|---------| +| **Decision ID on pump events** | `decisionId: UUID?` added to dose entries and pump events across OmniKit, OmniBLE, and MinimedKit — links pump commands to the algorithm decision that triggered them (LOOP-5295) | +| **Pump inoperable state** | `inSignalLoss` and `isInoperable` properties added to LibreTransmitter, OmniBLE, and OmniKit for consistent inoperable-device reporting (LOOP-4801) | +| **acknowledgeAlert async migration** | OmniBLE and OmniKit: `acknowledgeAlert` migrated from completion-handler to `async throws` | + +### Services + +| Feature | Details | +|---------|---------| +| **TidepoolService: decisionId** | `DoseEntry` now carries `decisionId` for Tidepool upload correlation | +| **NightscoutService: decisionId** | Same `decisionId` support added for Nightscout upload | + +--- + +## 2. Conflicts Encountered + +### LoopAlgorithm +**No conflicts.** Because DIY Loop had never incorporated this package before, there were +no diverging commits on the DIY side — the repo had been tracking `LoopKit/LoopAlgorithm` +main without any local modifications. The merge was a clean fast-forward: Tidepool was +29 commits ahead, all of which applied without conflict. + +--- + +### LoopKit +**16 conflicts** across Swift source and project files. + +| File | Conflict Type | +|------|--------------| +| `LoopKit.xcodeproj/project.pbxproj` | File references, deployment target, `.strings` vs `.xcstrings` | +| Multiple algorithm files in `LoopKit/LoopAlgorithm/` | HealthKit → LoopUnit type migration | +| Various type definition files | API additions on both sides | + +--- + +### Loop (App) +**33 conflicts** across 30+ files — the most complex repo in the sync. + +| Category | Count | Files | +|----------|-------|-------| +| Modify/delete | 3 | `SystemActionLink.swift`, `FavoriteFoodDetailView.swift`, `Main.strings` | +| Swift source | 22 | Managers, ViewControllers, ViewModels, Views, Core/Tests | +| xcschemes | 7 | All scheme files | +| project.pbxproj | 1 | Build settings, file references | + +**Deepest conflict: `LoopDataManager.swift`** — 7 conflict hunks due to a fundamental architectural divergence: +- Tidepool migrated to Swift Concurrency (`@MainActor async/await`, `Task {}`) +- DIY added Live Activity support and retained `dataAccessQueue`-based threading + +--- + +### Peripheral Repos (9 repos) +**pbxproj-only conflicts** — no Swift source conflicts. + +| Repo | Conflict | +|------|----------| +| CGMBLEKit | project.pbxproj | +| G7SensorKit | project.pbxproj | +| dexcom-share-client-swift | project.pbxproj | +| NightscoutRemoteCGM | project.pbxproj | +| RileyLinkKit | project.pbxproj | +| LoopSupport | project.pbxproj | +| LoopOnboarding | project.pbxproj | +| AmplitudeService | project.pbxproj | +| LogglyService | project.pbxproj | + +**Swift source conflicts in plugin repos:** + +| Repo | Files | Conflict | +|------|-------|---------| +| OmniKit | `OmnipodPumpManager.swift`, `PodCommsSessionTests.swift` | decisionId, async acknowledgeAlert | +| OmniBLE | `OmniBLEPumpManager.swift`, `PodState.swift` | decisionId, slot6SuspendTimeExpired guard, async acknowledgeAlert | +| MinimedKit | `MinimedPumpManager.swift` | decisionId, async Task, updateLastEventDates | +| LibreTransmitter | `LibreTransmitterManagerV3.swift` | inSignalLoss/isInoperable properties | +| TidepoolService | `DoseEntry+Tidepool.swift` | decisionId, import LoopAlgorithm | +| NightscoutService | `NightscoutService.swift` | decisionId, RemoteNotificationResponseManager | + +--- + +## 3. Conflict Resolution Decisions + +### LoopAlgorithm: HealthKit → LoopUnit Migration + +**Conflict:** Tidepool replaced `HKUnit`/`HKQuantity` HealthKit types throughout the algorithm +with custom `LoopUnit`/`LoopQuantity` types. LoopKit DIY's inline algorithm copy still used +HealthKit types. + +**Decision:** Accept Tidepool's type migration in full. `LoopUnit` and `LoopQuantity` are +functionally equivalent and improve portability. The inline LoopKit algorithm code was updated +to match. The `HKUnit.swift` and `HKQuantity.swift` extensions were removed. + +--- + +### Loop: `LoopDataManager.swift` — Concurrency Migration + +**Conflict:** The deepest conflict in the sync. Tidepool restructured `LoopDataManager` around +Swift Concurrency — replacing `dataAccessQueue.async` blocks with `Task { @MainActor in }` and +`await`-based calls. DIY had added `LiveActivityManager` integration woven into the same +`dataAccessQueue` paths, and retained `lockedSettings`/`mutateSettings()`/`loop()`/`loopInternal()`/`finishLoop()`. + +**Decision:** +- Adopt Tidepool's `Task { @MainActor in await updateDisplayState() }` pattern as the new + threading model throughout +- Remove DIY's `dataAccessQueue`, `lockedSettings`, `mutateSettings()`, `loop()`, + `loopInternal()`, and `finishLoop()` chain — Tidepool's async pattern replaces this +- Inject DIY's Live Activity calls into Tidepool's new `updateDisplayState()` call sites + (hunks 3, 4, 5), so live activity updates fire from the same points the old queue callbacks did +- Adopt Tidepool's new init parameters (`analyticsServicesManager`, `carbAbsorptionModel`, + `usePositiveMomentumAndRCForManualBoluses`, `dosingStrategySelectionEnabled`) and move + all stored property assignments before the `overrideIntentObserver` closure to satisfy + Swift's init-before-capture requirement + +--- + +### Loop: `SystemActionLink.swift` — Deleted (replaced by DeeplinkView) + +**Conflict:** Tidepool deleted `SystemActionLink.swift` and replaced it with `DeeplinkView.swift`. +DIY had made improvements to `SystemActionLink` (widget rendering mode, active preset colors). + +**Decision:** Accept the deletion and take `DeeplinkView.swift`. It provides equivalent +deeplink functionality. DIY's widget tinting improvements should be ported to `DeeplinkView` +if verified missing. + +--- + +### Loop: `FavoriteFoodDetailView.swift` — Deleted (moved) + +**Conflict:** Tidepool moved the file to `Loop/Views/Favorite Foods/FavoriteFoodDetailView.swift`. +DIY had added `String(localized:)` annotations to the original location. + +**Decision:** Accept the move. The `Favorite Foods/` subfolder version already exists in DIY's +tree. DIY's localization annotations should be verified against the new location. + +--- + +### Loop: `Main.strings` — Keep Deleted + +**Conflict:** DIY deleted `Main.strings` when migrating to `.xcstrings` string catalogs. +Tidepool modified `Main.strings` (never migrated). + +**Decision:** Keep the deletion. DIY's `.xcstrings` format is the forward path. + +--- + +### Loop: `AppDelegate.swift` — Keep Both + +**Conflict:** DIY added diagnostic logging setup; Tidepool added an XCTest environment guard +(skips full initialization during unit tests). + +**Decision:** Keep both — they are independent and complementary additions. + +--- + +### Loop: `DeviceDataManager.swift` — Keep Both + +**Conflict:** DIY added a diagnostic report function with submodule SHAs (`b6e88416`); Tidepool +added `roundBasalRate(unitsPerHour:)` (`184ea75a`). Both additions were at adjacent but +non-overlapping locations. + +**Decision:** Keep both — distinct features, no overlap. + +--- + +### Loop: `SettingsView.swift` — Keep Both + +**Conflict:** DIY had a Favorite Foods sheet; Tidepool added a Presets sheet. + +**Decision:** Keep both sheets — separate features serving different user needs. + +--- + +### Loop: `NSUserDefaults.swift` — Keep Both + +**Conflict:** DIY added `liveActivity` key (`LiveActivitySettings`); Tidepool added +`defaultEnvironment` key. + +**Decision:** Keep both — entirely separate features (Live Activity vs Tidepool service config). + +--- + +### Loop: `WidgetBackground.swift` — Take Tidepool's + +**Conflict:** Tidepool uses `Color.widgetBackground` extension; DIY used `Color("WidgetBackground")` +string-based lookup. + +**Decision:** Take Tidepool's — the extension is safer (compile-time vs. runtime string lookup). + +--- + +### Loop: `ContentMargin.swift` — Keep DIY's + +**Conflict:** Both sides had identical implementation; only the copyright year differed. + +**Decision:** Keep DIY's copyright date. + +--- + +### OmniBLE: `slot6SuspendTimeExpired` Guard — Keep DIY's + +**Conflict:** Tidepool's version of OmniBLE removed the `slot6SuspendTimeExpired` safety check +that prevents acknowledging a pod alert when the pod is suspended. + +**Decision:** Preserve DIY's guard. This is a safety-critical check: if the pod is suspended, +the pod should continue beeping until the user resumes it. Silently acknowledging in this state +could mask a dangerous condition. + +--- + +### MinimedKit: `updateLastEventDates` — Keep DIY's + +**Conflict:** Tidepool's MinimedKit removed `updateLastEventDates()`, a function DIY uses to +track cannula age and insulin age for display in the UI. + +**Decision:** Preserve DIY's implementation. Cannula/insulin age tracking is a meaningful DIY +feature with no equivalent in Tidepool's version. + +--- + +### NightscoutService: `RemoteNotificationResponseManager` — Keep DIY's + +**Conflict:** Tidepool's NightscoutService version removed `RemoteNotificationResponseManager`, +which handles feedback notifications for remote commands sent via Nightscout. + +**Decision:** Preserve DIY's implementation. Remote command feedback is a core DIY feature +(remote bolus, temp basal, etc.) that has no Tidepool equivalent. + +--- + +### TidepoolService: `import LoopAlgorithm` — Removed + +**Conflict:** Tidepool's `TidepoolService` imports `LoopAlgorithm` as a Swift package. DIY does +not include the `LoopAlgorithm` package in the workspace — the algorithm is embedded inline +inside `LoopKit`. + +**Decision:** Remove `import LoopAlgorithm` from `TidepoolService`. In DIY, the types it +provided come from `LoopKit` which is already imported. + +--- + +### All Repos: project.pbxproj + +Applied consistent rules across all 18 repos: + +| Setting | Decision | Rationale | +|---------|----------|-----------| +| `IPHONEOS_DEPLOYMENT_TARGET` | Take higher value (17.0) | Both sides are raising it; take the further-along value | +| `LOCALIZATION_PREFERS_STRING_CATALOGS` | Keep `YES` | DIY's xcstrings migration | +| `.strings` file references | Drop Tidepool's | DIY deleted these files; re-adding references causes build errors | +| `.xcstrings` references | Keep DIY's | Current localization format | +| `XCRemoteSwiftPackageReference "LoopAlgorithm"` | Omit | DIY embeds algorithm inline in LoopKit | +| New Swift file references | Keep both sides' additively | Both sides added files; all should be included | +| PBXGroup `children`/`files` duplicates | Deduplicate after merge | "Keep both" strategy on adjacent group entries can produce duplicates | +| Bundle IDs (`com.loopkit.*` / `com.tidepool.*`) | Keep both | Apply to different build targets | + +--- + +## 4. DIY Features Preserved + +| Feature | Repo | Notes | +|---------|------|-------| +| **Live Activity** (`LiveActivityManager`) | Loop | Woven into Tidepool's new `updateDisplayState()` async calls | +| **Diagnostic report with submodule SHAs** | Loop | Kept alongside Tidepool's `roundBasalRate` addition | +| **Favorite Foods sheet** | Loop | Kept alongside Tidepool's new Presets sheet | +| **liveActivity NSUserDefaults key** | Loop | Kept alongside Tidepool's `defaultEnvironment` key | +| **slot6SuspendTimeExpired safety guard** | OmniBLE | Safety-critical; not present in OmniKit (different pod hardware) | +| **updateLastEventDates** | MinimedKit | Cannula/insulin age tracking | +| **RemoteNotificationResponseManager** | NightscoutService | Remote command feedback | +| **`.xcstrings` localization** | All repos | DIY's Xcode 15+ string catalog format | +| **Community CGM integrations** | CGMBLEKit, LibreTransmitter, etc. | Open-source CGM drivers not in Tidepool | +| **Open-source pump drivers** | OmniKit, OmniBLE, MinimedKit, RileyLinkKit | All preserved | + +--- + +## 5. Post-Merge Fixes + +After the mechanical merge, the following additional fixes were required to achieve a +clean build: + +### `LoopDataManager.swift` — Init Property Ordering (Loop) + +**Problem:** Swift requires all stored properties to be initialized before `self` can be +captured in a closure. The `overrideIntentObserver` closure captured `self`, but several +stored properties (`analyticsServicesManager`, `carbAbsorptionModel`, +`usePositiveMomentumAndRCForManualBoluses`, `automationHistory`, +`publishedMostRecentGlucoseDataDate`, `dosingStrategySelectionEnabled`, +`publishedMostRecentPumpDataDate`) were assigned *after* the closure in the init. + +**Fix:** Moved all stored property assignments before the `overrideIntentObserver` closure. + +--- + +### `NSUserDefaults.swift` — Missing Closing Braces (Loop) + +**Problem:** The merge introduced a structural artifact in the `liveActivity` computed property +— the closing `}}` for the `set {` block was missing. + +**Fix:** Restored the two missing closing braces manually. + +--- + +### `TidepoolServiceKit/Extensions/DoseEntry.swift` and `DeviceLogUploader.swift` — Missing `import LoopAlgorithm` + +**Problem:** Both files use `AbsoluteScheduleValue` (DoseEntry.swift) and `TDeviceLogEntry` +(DeviceLogUploader.swift) from the LoopAlgorithm package, but `import LoopAlgorithm` was +incorrectly removed from both files during conflict resolution. All other ~10 files in +TidepoolServiceKit correctly import LoopAlgorithm. The omission caused build failures in +Xcode (though command-line builds may succeed due to implicit module visibility from +LoopKit.xcodeproj's LoopAlgorithm SPM dependency). + +**Fix:** Restored `import LoopAlgorithm` in both files. + +--- + +### `Loop.xcodeproj/project.pbxproj` — Structural Corruption (Loop) + +**Problem:** The pbxproj resolver's "keep both" strategy on a `PBXVariantGroup` conflict +(conflict 36) produced a combined brace depth of +2 instead of 0, making the file +unparseable by Xcode. + +**Root cause:** Both sides of the conflict had `brace_balance = +1`. Naively keeping both +produced `+2`, breaking the file's structure. + +**Fix:** +1. Re-ran `git merge-file` on origin/dev vs merge-base vs tidepool/dev to get clean + conflict markers +2. Applied improved resolver: checks `brace_count(ours) + brace_count(theirs)` before + keeping both; takes OURS when the combined balance would be non-zero +3. Deduplicated 7 duplicate lines in PBXGroup `children`/`files` lists (artifact of + "keep both" on adjacent group entries) +4. Validated with `xcodebuild -project Loop.xcodeproj -list` ✅ + +--- + +## 6. Testing Notes + +### Critical paths to verify + +- [ ] **Live Activity** — glucose, dosing, and carb update notifications all trigger live activity updates +- [ ] **Widget action buttons** — carbs, bolus, pre-meal, preset deeplinks work; colors correct in tinted/accented mode (DeeplinkView replaced SystemActionLink) +- [ ] **Favorite Foods** — detail view labels localized; "Save as favorite" only shown when no food selected +- [ ] **Settings** — both Favorite Foods and Presets sheets accessible +- [ ] **Bolus entry** — safe area layout correct (GeometryReader removed) +- [ ] **Presets training** — training flow completes; "+" button blocked until training done +- [ ] **Activity presets** — all 4 activity types appear; defaults are correct; "modified" indicator shows when changed +- [ ] **High insulin needs mitigation** — correction range clamped to ≥ 110 mg/dL when insulin needs > 165% +- [ ] **Indefinite preset reminder** — 24-hour alert fires when indefinite preset is active; retracts on deactivation +- [ ] **Preset scheduling** — scheduled presets alert at the correct time; "Yes, Start Now" activates correctly +- [ ] **Pre-meal guardrail** — max 130 mg/dL hard cap; recommended upper bound matches correction range lower bound +- [ ] **Active preset banner** — displays correctly on CarbEntryView when override active +- [ ] **Algorithm** — glucose predictions, ISF during carb absorption, max IOB multiplier +- [ ] **TempBasalRecommendation direction** — direction field populated in recommendations +- [ ] **Pump decision IDs** — OmniKit, OmniBLE, Minimed dose entries carry decisionId +- [ ] **Remote commands** — Nightscout remote bolus/basal feedback notifications work +- [ ] **Cannula/insulin age** — displays correctly for Minimed users (updateLastEventDates) +- [ ] **OmniBLE pod suspend safety** — suspended pod continues beeping; alert not silently acked +- [ ] **Diagnostic report** — support report includes submodule SHAs +- [ ] **App expiry alerts** — TestFlight and provisioning profile expiry handled correctly +- [ ] **Unit tests** — `LoopAlgorithmTests`, `LoopKitTests`, `LoopTests` suites pass diff --git a/docs/tidepool-sync-2026-05-11.md b/docs/tidepool-sync-2026-05-11.md new file mode 100644 index 0000000000..540b16d82f --- /dev/null +++ b/docs/tidepool-sync-2026-05-11.md @@ -0,0 +1,228 @@ +# Tidepool → LoopKit DIY Sync — 2026-05-11 + +**Branch:** `tidepool-sync/2026-05-11` (17 repos merged + LoopAlgorithm pin bump) +**Build status:** pending verification (in progress at time of writing) +**Previous sync:** 2026-03-10 (see [`tidepool-sync-2026-03-10.md`](tidepool-sync-2026-03-10.md)) + +This is the smaller, follow-up sync after the large 2026-03-10 rebase. +Most of the heavy architectural integration (LoopAlgorithm package extraction, +Swift Concurrency migration, HK→LoopUnit migration) landed last time. This sync +absorbs roughly 2 months of incremental Tidepool development. + +--- + +## Headline numbers + +| Repo (Tier 1+2) | Tidepool commits absorbed | +|---|---| +| LoopKit | 409 | +| Loop | 14 | + +| Repo (Tier 3) | Tidepool commits absorbed | +|---|---| +| TidepoolService | 45 | +| LoopOnboarding | 28 | +| NightscoutService | 22 | +| OmniBLE | 20 | +| OmniKit / MinimedKit | 18 each | +| G7SensorKit / dexcom-share-client-swift | 15 each | +| LibreTransmitter | 14 | +| CGMBLEKit / NightscoutRemoteCGM | 13 each | +| LoopSupport | 11 | +| AmplitudeService / LogglyService | 6 each | +| RileyLinkKit | 3 | +| MixpanelService | 0 (already up to date) | +| LoopAlgorithm (package pin) | 4 (test-only) | + +Total: ~660 Tidepool commits across the ecosystem. + +--- + +## 1. New features from Tidepool + +Most of these are completed by Tidepool in the LoopKit/Loop repos themselves. +The plugins primarily got matching protocol/API updates. + +### LoopKit + +- **Required version-update flow** (LOOP-1114) — new `LoopNotificationCategory.requiredUpdate` + and a `SupportProviding` protocol with `MockSupport` UI. +- **`isMutable` dose detection** (LOOP-5843) — `DoseStore` now uses `isMutable` rather + than time-based heuristics to determine unfinished doses. +- **Activity preset insulin-scale tuning** (LOOP-5807) — biking 0.22→0.23, strength + training 0.39→0.37 in `TemporaryScheduleOverride.defaultInsulinNeedsScaleFactor`. +- **Correction range overrides guardrail** (LOOP-5878) — `CorrectionRangeOverridesEditor` + now actually passes `viewModel.guardrail` (was always `nil`). +- **New preset UI infrastructure** — `EditPresetView`, `ReviewNewPresetView`, + `InsulinNeedsAdjustmentPreview`, and supporting types. +- **Media/Transcript support** — 7 new files under `LoopKit/Media/` (captions, transcripts, + metadata) — likely for in-app support/training video infrastructure. +- **QuantityFormatter API simplification** — removed unused `rule:` parameter from + `doubleValue()`. + +### Loop + +- **Required version-update view** — `RequiredVersionUpdateView`, paired with the LoopKit + notification category above. +- **Preset performance history** — `PresetPerformanceHistoryView` and `PresetsPerformanceHistoryViewModel`. +- **Automation history tracking** — `AutomationHistoryEntry`, `AutomationHistoryEntryTests`. +- **Media/Transcript player infrastructure** — `AudioPlayer`, `CaptionsView`, `MediaPlayerView`, + `PlayerControls`, `TranscriptView`, `VideoView` under `Loop/Views/Presets/Media Player/`. + +### Plugins + +- **OmniBLE / OmniKit** — `mutateState` API migration (replacing `setState`); `decisionId` + carried through the temp-basal path; pod-inoperable refinements. +- **MinimedKit** — same `decisionId` + protocol updates as the other pump drivers. +- **TidepoolService / NightscoutService** — `decisionId` on `DoseEntry` and + `PersistedPumpEvent`; misc protocol updates. + +--- + +## 2. Conflicts encountered & resolutions + +### LoopKit — 18 source conflicts + 19 pbxproj regions + +**Mechanical "take Tidepool" (low risk):** +DoseStore, LoopNotificationCategory, QuantityFormatter, TemporaryScheduleOverride, +LoopKitUI/SupportUI, GlucoseTherapySettingInformationView, CorrectionRangeOverridesEditor, +InsulinType (loses Lokalise detailed insulin descriptions — translations will repopulate +on next Lokalise run). + +**Keep DIY (DIY-only debug features):** +`MockKitUI/Views/MockCGMManagerSettingsView.swift` and `MockPumpManagerSettingsView.swift` +— simulator settings gated by `allowDebugFeatures`. Tidepool doesn't have these. + +**Accept Tidepool deletions (LoopAlgorithm package extraction, LOOP-4781):** +- `LoopKit/InsulinKit/ExponentialInsulinModel.swift` +- `LoopKit/LoopAlgorithm/LoopAlgorithm.swift` +- `LoopKit/LoopAlgorithm/LoopPredictionOutput.swift` + +These types now live in `tidepool-org/LoopAlgorithm` (the Swift Package DIY already +pulls via workspace `Package.resolved`). Deleting the inline copies makes DIY match +Tidepool's architecture. + +**Accept Tidepool deletion (preset UI replaced):** +`LoopKitUI/View Controllers/OverrideSelectionViewController.swift` — superseded by +Tidepool's new SwiftUI `EditPresetView` / `ReviewNewPresetView`. DIY's crash-fix +commit `3ce43ded` does not apply to the new SwiftUI flow. + +**Add/add — both sides added the same files:** +Took Tidepool's content for the new preset infrastructure +(`InsulinNeedsAdjustmentPreview`, `EditPresetView`, `ReviewNewPresetView`). + +**DIY divergence — BasalRateScheduleEditor max-basal filtering (see Section 4):** +The auto-merge silently dropped this fix; restored manually with a comment pointing +at the divergence memory. + +**pbxproj (19 regions):** +- Dropped all Tidepool `.strings` PBXFileReferences and PBXBuildFiles +- Kept DIY's `.xcstrings` references and `LOCALIZATION_PREFERS_STRING_CATALOGS = YES` +- Kept Tidepool's new Media/Transcript and `TimeInterval+Timecode.swift` references +- Dropped references to the 4 source files deleted in this merge +- Pre-existing `XCRemoteSwiftPackageReference "LoopAlgorithm"` in HEAD was left alone + (not part of this merge; same as 2026-03-10) +- Final state: 1898 `{` / 1898 `}`, `xcodebuild -list` parses + +### Loop — 3 pbxproj regions only (no source conflicts) + +- Regions 1 + 2: both sides added a Swift file at the same insertion point — + kept both `ContentMargin.swift` (DIY) and `PresetPerformanceHistoryView.swift` (Tidepool). +- Region 3: dropped Tidepool's `.strings` refs (ru/de InfoPlist/Localizable/ckcomplication); + kept Tidepool's new Swift files (`RequiredVersionUpdateView`, `AutomationHistoryEntry`, + `AutomationHistoryEntryTests`, `LoopCircleView`). +- Cleaned up 3 orphaned variant-group children at lines ~4088, 4147, 4166 that + referenced the dropped `.strings` FileReferences. + +### Plugins (Tier 3) — pbxproj patterns + +For 11 of the 15 plugin merges, the *only* conflict was the standard +`LOCALIZATION_PREFERS_STRING_CATALOGS = YES` line in the Debug+Release +XCBuildConfiguration blocks. All resolved by keeping DIY's setting. + +Notable plugin-specific cleanup: +- **CGMBLEKit**: dropped a duplicate array-form `LD_RUNPATH_SEARCH_PATHS` entry + (string form remains below). +- **dexcom-share-client-swift**: cleaned up an orphaned `Localizable.strings` + children-reference in the `ShareClient` PBXGroup that had no PBXFileReference + declaration. +- **TidepoolService**: 4 conflict regions — kept DIY's `.xcstrings` PBXBuildFiles + and PBXFileReferences (the 4 IDs are properly wired into PBXGroup children + and PBXResourcesBuildPhase); dropped 10 Tidepool `.strings` FileReferences. + +### Plugin source conflicts (the 5 repos that had them) + +| Repo / File | Resolution | +|---|---| +| OmniKit / `OmnipodPumpManager.swift` | 3 regions keep DIY (`isSignalLost(at:lastPumpDataReportDate:)` reentrant-lock fix from commit `924f10d`); 3 regions take Tidepool (`setState` → `mutateState` Swift 6 migration). | +| OmniBLE / `OmniBLEPumpManager.swift` | 3 regions keep DIY (reentrant-lock fix `e9425ad`); region at line 2221 keep DIY (`completion(.communication(error))` error style — Tidepool's do/catch refactor cannot be cleanly applied to the conflict region alone; `decisionId` tracking already present in DIY's `setTempBasal` call); region at line 2722 **manual merge** (preserve DIY suspend-time-expired special case from Pod Keep Alive #165 + adopt Tidepool's properly-indented `try await withCheckedThrowingContinuation`); 3 regions take Tidepool (`mutateState`). | +| MinimedKit / `MinimedPumpManager.swift` | All 3 regions keep DIY — preserves CAGE/IAGE tracking (commit `ff07802`); the third region was Tidepool adding a duplicate `isInoperable` property that DIY already had elsewhere in the file. | +| MinimedKit / `MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift` | Take Tidepool (trivial whitespace). | +| TidepoolService / `TidepoolServiceKit/Extensions/DoseEntry.swift` | Both sides changed import ordering; pre-staged resolution kept both → duplicate `import LoopAlgorithm`. Follow-up commit `5f6a064` deduped. | +| NightscoutService / `NightscoutServiceKit/NightscoutService.swift` | Keep DIY — preserves the 60-line APNS response feature (`RemoteNotificationResponseManager`, JWT-managed return notifications) from commit `0ca2c08`. Tidepool's version is a simpler switch with no response handling. | + +--- + +## 3. DIY divergences + +See `SYNC_PROGRESS.md` "DIY Divergences" section for the canonical list. New as of this sync: + +### BasalRateScheduleEditor max-basal filtering + +A DIY user (OmniBLE/Dash) reported being able to set basal schedule entries above +their configured `maximumBasalRatePerHour`. The cause was that DIY had inherited +Tidepool's PR #734 (LOOP-5767, "Basal schedule editor should ignore max basal rate", +merged 2026-02-27) in the 2026-03-10 sync, which changed the convenience initializer +in `BasalRateScheduleEditor` to pass `maximumBasalRate: nil`, disabling the picker-side +filter. + +DIY explicitly rejects this Tidepool change. Restored +`maximumBasalRate: therapySettingsViewModel.therapySettings.maximumBasalRatePerHour` +with an inline comment pointing at `memory/divergence_basal_max_filter.md`. On +2026-05-11 sync, auto-merge silently reverted this fix; manual restoration was needed +and a divergence comment was added to defend it on future syncs. + +--- + +## 4. Items to verify post-build + +- **OmniBLE temp basal error reporting:** kept DIY's `completion(.communication(error))` + pattern; verify error messages still surface correctly to Loop on temp basal failures. +- **Mock simulator features:** ensure simulator pump/CGM settings still render under + `allowDebugFeatures` flag in debug builds. +- **APNS responses (NightscoutService):** verify remote command feedback notifications + still flow end-to-end. +- **Basal schedule editor (OmniBLE / Dash):** verify the originally reported bug is + resolved — entries above the configured max basal should not be selectable. +- **Lokalise translations:** `InsulinType.swift` lost DIY's detailed insulin + descriptions in favor of Tidepool's simpler combined Fiasp/Lyumjev case. Next + Lokalise pull should repopulate. + +--- + +## 5. Upgrading is a one-way operation (Core Data) + +**You cannot revert to `dev` after upgrading to the sync build.** + +The shared LoopKit Core Data store (glucose, dose, carb, dosing decisions — all in one +`Model.sqlite` in the app group) is migrated forward from **Modelv4** to **Modelv6** on +first launch of the sync build. `dev` only ships model versions up to **Modelv4** (it has +no v5/v6 model), and Core Data migrations are forward-only, so a `dev` build cannot open a +v6 store: `addPersistentStore` fails and `PersistenceController` lands in an `.error` state. + +Consequences of going back to `dev` after upgrading: +- `dev`'s data stores won't load — the app is non-functional (no cached glucose/dose/carb, + looping won't run). +- The v6 data is **not** wiped from disk, so reinstalling the sync build reads it again. +- To actually use `dev` again you must delete + reinstall it, which wipes the local cache; + it then rebuilds from HealthKit (the long-term history lives there, not in this store). + +This is inherent Core Data forward-migration behavior, not specific to any one change. + +**Forward migration preserves insulin data.** The v4→v6 mapping originally dropped the +old single `value` attribute (auto-generated, name-based mappings had no destination), +zeroing cached bolus/basal amounts and understating IOB. `CachedInsulinDeliveryObjectMigrationPolicy` +now copies `value` → `deliveredUnits` (and `programmedUnits` for boluses); basal *rates* +already carry over via `scheduledBasalRate`/`programmedTempBasalRate`. Note this only fixes +the forward path — installs that already migrated on a build *without* the policy have +already-dropped values that this cannot recover (they read as 0). diff --git a/sync-docs/Loop.md b/sync-docs/Loop.md new file mode 100644 index 0000000000..57292038ca --- /dev/null +++ b/sync-docs/Loop.md @@ -0,0 +1,158 @@ +# Loop Sync Log + +**Repo:** https://github.com/LoopKit/Loop +**Tidepool fork:** https://github.com/tidepool-org/Loop +**Sync date:** 2026-03-10 +**Sync branch:** `tidepool-sync/2026-03-10` +**Base branch:** `dev` + +--- + +## Merge Summary + +- Merge base: `55cf35a9` +- 33 conflicts across 30+ files +- Categories: 3 modify/delete, 22 Swift source, 7 xcschemes, 1 pbxproj + +--- + +## Modify/Delete Resolutions + +### `Loop Widget Extension/Components/SystemActionLink.swift` — DELETED +- **Tidepool** deleted it in commit `c22b37f4` ("Code cleanup"), replacing with `DeeplinkView.swift` +- **DIY** had improved it: added `@available(iOS 16.1, *)`, `widgetRenderingMode`, fixed active preset colors +- **Resolution:** Take deletion. `DeeplinkView.swift` auto-merged cleanly and provides the same + deeplink functionality. DIY's widget tinting improvements should be ported to `DeeplinkView` + if they're missing. +- ⚠️ **Test:** Widget action buttons (carbs, bolus, pre-meal, preset) — verify colors in accented/tinted mode + +### `Loop/Views/FavoriteFoodDetailView.swift` — DELETED (moved) +- **Tidepool** moved it to `Loop/Views/Favorite Foods/FavoriteFoodDetailView.swift` in commit + `2c914f87` ("Renaming/organizing favorite foods") +- **DIY** had updated it with `String(localized:)` annotations on the same fields +- **Resolution:** Take deletion. The `Favorite Foods/` subfolder version already exists in DIY's + tree and `FavoriteFoodsView.swift` references it correctly. DIY's localization updates need + to be checked against the Favorite Foods subfolder version. +- ⚠️ **Test:** Favorite Foods detail view — verify all field labels are localized + +### `Loop/en.lproj/Main.strings` — KEPT DELETED +- DIY deleted it in `cfff59a5` (migrated to `.xcstrings` format) +- Tidepool modified it (never migrated to xcstrings) +- **Resolution:** Keep deleted per the standard .strings policy + +--- + +## Swift Source Resolutions + +### Widget Files + +| File | Resolution | Notes | +|------|-----------|-------| +| `ContentMargin.swift` | Kept DIY's copyright date | Same impl, only date differed | +| `WidgetBackground.swift` | Took Tidepool's | `Color.widgetBackground` extension cleaner than `Color("WidgetBackground")` | +| `SystemStatusWidget.swift` | Took Tidepool's | Uses `DeeplinkView` + `.containerRelativeBackground()`; removed `@available(iOS 16.1, *)` guard (no longer needed) | + +### Managers + +| File | Hunks | Resolution | Notes | +|------|-------|-----------|-------| +| `AppDelegate.swift` | 1 | Kept both | DIY: logging; Tidepool: XCTest environment guard (skip full init in unit tests) | +| `StoredAlert.swift` | 2 | Took Tidepool's | Whitespace/empty hunks only | +| `AppExpirationAlerter.swift` | 2 | Took Tidepool's | Indentation of `#if targetEnvironment(simulator)` only | +| `DeviceDataManager.swift` | 1 | Kept both | DIY: diagnostic report with submodule SHAs (`b6e88416`); Tidepool: `roundBasalRate(unitsPerHour:)` function (`184ea75a`) — different code at adjacent location | +| `RemoteDataServicesManager.swift` | 1 | Took Tidepool's | Migrated `uploadCgmEventData` from callback to `async/await Task` | + +### LoopDataManager ⚠️ NEEDS COMPILE TEST + +This file has the deepest architectural conflict. Tidepool migrated to Swift Concurrency +(`@MainActor async/await`) while DIY added Live Activity support and kept `dataAccessQueue`. + +| Hunk | DIY | Tidepool | Resolution | +|------|-----|---------|-----------| +| 1 | `liveActivityManager: LiveActivityManagerProxy?` property | `lastReservoirValue` computed property | **Kept both** | +| 2 | LiveActivityManager init + overrideIntentObserver setup | Different init params (`analyticsServicesManager`, `carbAbsorptionModel`, etc.) | **Kept both** — ⚠️ likely compile errors; needs human review | +| 3 | `dataAccessQueue.async` + cache invalidation + liveActivity update | `Task { @MainActor in await updateDisplayState() }` | **Tidepool's Task + liveActivity injected** | +| 4 | `dataAccessQueue.async` + glucose effect clear + liveActivity | `Task { @MainActor }` + `restartGlucoseValueStalenessTimer()` | **Tidepool's Task + liveActivity injected** | +| 5 | `dataAccessQueue.async` + insulin effect clear + liveActivity | `Task { @MainActor in await updateDisplayState() }` | **Tidepool's Task + liveActivity injected** | +| 6 | `lockedSettings`, `settings`, `mutateSettings()` + liveActivity calls | `Task { await updateDisplayState() }` | **Kept both** — ⚠️ structural divergence; needs human review | +| 7 | Complete `loop()`→`loopInternal()`→`finishLoop()`→`update()` chain | `await dosingDecisionStore.storeDosingDecision()` (async) | **Kept both** — ⚠️ DIY loop chain critical; Tidepool async store; needs human review | + +**Key questions for human review:** +- Did Tidepool replace `lockedSettings`/`mutateSettings()` with a different pattern? If so, all callers need updating. +- Does Tidepool's `updateDisplayState()` need to also call `liveActivityManager?.update()`? +- Is the DIY `loop()`/`loopInternal()` chain still intact or did Tidepool restructure it? + +### View Controllers + +| File | Resolution | Notes | +|------|-----------|-------| +| `CarbAbsorptionViewController.swift` | Took Tidepool's | Cleaner async do/catch pattern for carb review | +| `InsulinDeliveryTableViewController.swift` | Took Tidepool's | `pumpEvent.dose` + `String(describing:)` format | +| `StatusTableViewController.swift` | Took Tidepool's | More complete `.inProgress` switch handling | + +### View Models + +| File | Resolution | Notes | +|------|-----------|-------| +| `CarbEntryViewModel.swift` | Took Tidepool's | `CarbMath.dateAdjustmentPast` (from LoopAlgorithm pkg) vs `LoopConstants.maxCarbEntryPastTime` — same value, different source | +| `SimpleBolusViewModel.swift` | Took Tidepool's | Explicit `NSLocalizedString("–", comment:...)` | + +### Views + +| File | Resolution | Notes | +|------|-----------|-------| +| `BolusEntryView.swift` | Took Tidepool's | Removed `GeometryReader` wrapper; updated `onChange(of:)` to iOS 17+ two-param API | +| `CarbEntryView.swift` | Took Tidepool's | "Save as favorite" button only shown when `selectedFavoriteFood == nil` | +| `ManualEntryDoseView.swift` | Took Tidepool's | Minor indentation + iOS 17+ `onChange` API | +| `SettingsView.swift` | Kept both | DIY's `favoriteFoods` sheet + Tidepool's `presets` sheet added | + +### Core / Tests + +| File | Resolution | Notes | +|------|-----------|-------| +| `NSUserDefaults.swift` | Kept both | DIY: `liveActivity` key (`LiveActivitySettings`); Tidepool: `defaultEnvironment` key — separate features | +| `AlertStoreTests.swift` | Took Tidepool's | Updated JSON encoding assertions for new Alert format | +| `StoredAlertTests.swift` | Took Tidepool's | Updated JSON expectations | + +### xcschemes (all 7) + +Took Tidepool's — updated Xcode scheme versions. + +--- + +## project.pbxproj + +- Standard resolver applied: merged both sides' file references, dropped Tidepool's `.strings` refs, kept DIY's `.xcstrings`, took Tidepool's deployment target + +--- + +## Features Added by This Merge (Tidepool → DIY) + +- **Active Preset Banner** on CarbEntryView (LOOP-5432) +- **DeeplinkView** widget component (replaces SystemActionLink) +- **roundBasalRate** in DeviceDataManager (LOOP-5558) +- **schedulePresets** storage in NSUserDefaults (LOOP-4754) +- **defaultEnvironment** key for Tidepool service (LOOP-5153) +- **iOS 17+ onChange API** updates throughout views +- **XCTest environment guard** in AppDelegate (skip full init when testing) +- **Async Task pattern** in RemoteDataServicesManager + +## DIY Features Preserved + +- **Live Activity** (`LiveActivityManager`, `liveActivityManager?.update()` calls woven into Tidepool's Task blocks) +- **Diagnostic report with submodule SHAs** in DeviceDataManager +- **FavoriteFoods** sheet in SettingsView +- **liveActivity** NSUserDefaults key + `LiveActivitySettings` + +--- + +## Testing Notes + +- [ ] ⚠️ **Build** — `LoopDataManager.swift` hunks 2/6/7 kept both sides and will very likely need manual fixup to compile +- [ ] **Live Activity** — verify glucose, dosing, and carb notifications all trigger liveActivity updates (hunks 3/4/5) +- [ ] **Widget action buttons** — carbs, bolus, pre-meal, preset deeplinks work; colors correct in tinted mode +- [ ] **Favorite Foods** — detail view labels localized; "Save as favorite" only shown when no food selected +- [ ] **Settings** — both Favorite Foods and Presets sheets accessible +- [ ] **Bolus entry** — recommendation clears on edit; safe area layout correct (GeometryReader removed) +- [ ] **App expiry alerting** — TestFlight and profile expiry both handled independently +- [ ] **Diagnostic report** — includes submodule SHAs in support report diff --git a/sync-docs/LoopAlgorithm.md b/sync-docs/LoopAlgorithm.md new file mode 100644 index 0000000000..a5a98238ff --- /dev/null +++ b/sync-docs/LoopAlgorithm.md @@ -0,0 +1,143 @@ +# LoopAlgorithm Sync Log + +**Repo:** https://github.com/LoopKit/LoopAlgorithm +**Tidepool fork:** https://github.com/tidepool-org/LoopAlgorithm +**Sync date:** 2026-03-10 +**Sync branch:** `tidepool-sync/2026-03-10` +**Base branch:** `main` + +--- + +## Merge Summary + +**Type:** ✅ Clean fast-forward — no conflicts + +- Merge base (LoopKit/main tip): `9d24054` +- Tidepool/main tip: `13cb4b4` +- LoopKit has 0 commits not in Tidepool +- Tidepool has 29 commits not in LoopKit (across multiple PRs) + +``` +git checkout -b tidepool-sync/2026-03-10 +git merge --no-edit tidepool/main +# Result: Fast-forward, 67 files changed, 2237 insertions(+), 442 deletions(-) +``` + +--- + +## What Was Merged + +### PR #24 — Move glucose math tests from LoopKit to LoopAlgorithm +- Commit: `13cb4b4` +- https://github.com/tidepool-org/LoopAlgorithm/pull/24 +- Adds `GlucoseMathTests.swift` (435 lines) and associated fixtures +- Moves test coverage that was previously in LoopKit into this package +- **Testing impact:** Run `LoopAlgorithmTests` test suite + +### PR #23 — Gradual transitions support +- Commit: `8093b57` +- https://github.com/tidepool-org/LoopAlgorithm/pull/23 +- Adds support for gradual insulin/carb effect transitions in algorithm +- **Testing impact:** Verify glucose prediction curves match expected shapes + +### PR #22 — LOOP-5502: Allow setting of max active insulin multiplier +- Commit: `89dd58a` +- https://github.com/tidepool-org/LoopAlgorithm/pull/22 +- Adds configurable `maxActiveInsulinMultiplier` to algorithm input +- **Testing impact:** Verify max IOB calculations respect the multiplier + +### PR #21 — Carb absorption model selection updates +- Commit: `29c7b52` +- https://github.com/tidepool-org/LoopAlgorithm/pull/21 +- Updates how carb absorption model is selected/configured +- **Testing impact:** CarbMathTests; verify carb absorption curves + +### PR #20 — Fix decoding of old AutomaticDoseRecommendation structures +- Commit: `7ba61e1` +- https://github.com/tidepool-org/LoopAlgorithm/pull/20 +- Fixes backward compatibility for `AutomaticDoseRecommendation` without `basalAdjustment` +- **Testing impact:** Data migration; old stored recommendations should decode without crashing + +### PR #19 — Fix issue with mid-absorption ISF calculation +- Commit: `84a099f` +- https://github.com/tidepool-org/LoopAlgorithm/pull/19 +- Fixes insulin sensitivity factor calculation during active carb absorption +- **Testing impact:** CorrectionDosingTests; verify ISF used correctly during meal absorption + +### LOOP-5295 — Add directionality to TempBasalRecommendation +- PR: https://github.com/tidepool-org/LoopAlgorithm/pull/18 +- Adds `.direction` property to `TempBasalRecommendation` (increase/decrease/unchanged) +- Enables Loop UI to show directional feedback on temp basal changes +- **Testing impact:** Verify temp basal recommendations include direction field + +### LOOP-5280 — Display Glucose Preference by InternationalUnit +- Adds `LoopUnit` and `LoopQuantity` types to replace HealthKit dependency for glucose units +- **Testing impact:** New `LoopUnitTests.swift` added + +### Remove HealthKit Dependency & Upgrade to Swift 6 +- Removes `HKUnit.swift` and `HKQuantity.swift` extensions +- Replaces with `LoopUnit` and `LoopQuantity` (custom, no HealthKit import) +- Upgrades Swift concurrency to Swift 6 (`Sendable` annotations throughout) +- **Testing impact:** ⚠️ HIGH IMPACT — changes the unit/quantity types used in algorithm API. + Any caller of the algorithm API that used `HKUnit`/`HKQuantity` needs to be updated + to use `LoopUnit`/`LoopQuantity`. This is a breaking API change. + In LoopKit DIY: the inline `LoopKit/LoopAlgorithm/` code will need these types added, + OR LoopKit should adopt this package. + +### Remove CoreData Import +- Removed CoreData framework dependency from the package +- Package is now more portable and framework-independent + +### PR #14 — Mark AbsoluteScheduleValue as Sendable +- Adds Swift 6 `Sendable` conformance to `AbsoluteScheduleValue` + +--- + +## ⚠️ Breaking API Change: HealthKit Removal + +The most significant change is the replacement of `HKUnit`/`HKQuantity` with `LoopUnit`/`LoopQuantity`. + +**Impact on LoopKit DIY's inline algorithm code (`LoopKit/LoopKit/LoopAlgorithm/`):** +- The inline code still uses HealthKit types +- When syncing LoopKit, these new types need to either: + 1. Be introduced inline in `LoopKit/LoopAlgorithm/` as well, OR + 2. Prompt a decision to adopt the `LoopAlgorithm` package in LoopKit DIY + +This is a cross-repo dependency: **LoopAlgorithm sync must be reviewed before LoopKit sync**. +Specifically, any LoopKit conflict in `LoopAlgorithm/*.swift` files likely involves +these same HealthKit→LoopKit unit type changes. + +--- + +## Files Changed + +| Change | Files | +|--------|-------| +| Deleted | `Extensions/HKQuantity.swift`, `Extensions/HKUnit.swift` | +| Added | `LoopQuantity.swift`, `LoopUnit.swift`, `Tests/GlucoseMathTests.swift`, `Tests/LoopUnitTests.swift` | +| Added fixtures | 13 JSON test fixture files for glucose/carb math tests | +| Modified | `LoopAlgorithm.swift`, `DoseMath.swift`, `GlucoseMath.swift`, `CarbMath.swift`, `InsulinMath.swift`, `LoopPredictionInput.swift`, `AlgorithmInput.swift`, `TempBasalRecommendation.swift`, `AutomaticDoseRecommendation.swift`, and others | + +--- + +## Status + +✅ **Merged cleanly** — fast-forward, no conflicts. + +⚠️ **Action required before LoopKit sync:** +- Review the `LoopUnit`/`LoopQuantity` API change impact on LoopKit's inline algorithm code +- Decide: adopt LoopAlgorithm as a package in DIY, or update inline code to match? +- Consider whether PR needs to be opened on `LoopKit/LoopAlgorithm` → `main` + +--- + +## Testing Notes + +After this sync is pushed and integrated: +- [ ] Run full `LoopAlgorithmTests` test suite +- [ ] Verify glucose prediction accuracy (GlucoseMathTests) +- [ ] Verify ISF handling during carb absorption (CorrectionDosingTests) +- [ ] Verify carb absorption model selection +- [ ] Verify `AutomaticDoseRecommendation` backward decode compatibility +- [ ] Verify `TempBasalRecommendation` direction field populated +- [ ] Check for any callers of algorithm API still using `HKUnit`/`HKQuantity`