diff --git a/.circleci/config.yml b/.circleci/config.yml index ead42a398e..ef02ead944 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,18 +1,23 @@ jobs: build_and_test: macos: - xcode: 16.4 + xcode: 26.4 steps: - checkout - run: name: Checkout submodules command: git submodule update --init --recursive --depth 1 + - run: + name: Install xcbeautify + command: brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify - run: name: Build Loop - command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' build | xcpretty + command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 17' build | xcbeautify - run: name: Run Tests - command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' test | xcpretty + command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 17' test | xcbeautify --report junit --report-path test-results/results.xml + - store_test_results: + path: test-results workflows: version: 2 build_and_test: 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 f3472ea060..4a8f750208 100644 --- a/.gitmodules +++ b/.gitmodules @@ -52,12 +52,24 @@ [submodule "LibreTransmitter"] path = LibreTransmitter url = https://github.com/LoopKit/LibreTransmitter.git +[submodule "LibreLoop"] + path = LibreLoop + url = https://github.com/LoopKit/LibreLoop.git +[submodule "LoopAlgorithm"] + path = LoopAlgorithm + url = https://github.com/LoopKit/LoopAlgorithm.git +[submodule "LibreCRKit"] + path = LibreCRKit + url = https://github.com/airedev326/LibreCRKit.git [submodule "OmnipodKit"] path = OmnipodKit - url = https://github.com/loopandlearn/OmnipodKit -[submodule "MedtrumKit"] - path = MedtrumKit - url = https://github.com/jbr7rr/MedtrumKit + url = https://github.com/LoopKit/OmnipodKit.git + branch = next-dev [submodule "EversenseKit"] path = EversenseKit - url = https://github.com/bastiaanv/EversenseKit + url = https://github.com/LoopKit/EversenseKit.git + branch = loop-next-dev +[submodule "MedtrumKit"] + path = MedtrumKit + url = https://github.com/LoopKit/MedtrumKit.git + branch = next-dev 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/EversenseKit b/EversenseKit index 43b808002f..e7b1ad74cd 160000 --- a/EversenseKit +++ b/EversenseKit @@ -1 +1 @@ -Subproject commit 43b808002ffb88f29dc628cb609ca06eca8c7aeb +Subproject commit e7b1ad74cd5f7ddcc6619a29d1793652549e70ba 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/LibreCRKit b/LibreCRKit new file mode 160000 index 0000000000..66920c6d5a --- /dev/null +++ b/LibreCRKit @@ -0,0 +1 @@ +Subproject commit 66920c6d5a0d13c3047a1920d514c26b19e87cb4 diff --git a/LibreLoop b/LibreLoop new file mode 160000 index 0000000000..e0282d0633 --- /dev/null +++ b/LibreLoop @@ -0,0 +1 @@ +Subproject commit e0282d063323415b2ccec9906854e45d0c406195 diff --git a/LibreTransmitter b/LibreTransmitter index d0d301208f..6a940e83c0 160000 --- a/LibreTransmitter +++ b/LibreTransmitter @@ -1 +1 @@ -Subproject commit d0d301208faeb2bc763454baf0550f3fd4888bb7 +Subproject commit 6a940e83c002bde761b271f4d593186e53b3a891 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 1f71ec4fa9..a325b94301 160000 --- a/Loop +++ b/Loop @@ -1 +1 @@ -Subproject commit 1f71ec4fa94941abdbd72fd5bd914770faa2e90b +Subproject commit a325b94301395cd35d8988a8675bcefaf71952e0 diff --git a/LoopAlgorithm b/LoopAlgorithm new file mode 160000 index 0000000000..0ed893c1a7 --- /dev/null +++ b/LoopAlgorithm @@ -0,0 +1 @@ +Subproject commit 0ed893c1a75a355acd0b415b09ceebf2c3fb54c0 diff --git a/LoopConfigOverride.xcconfig b/LoopConfigOverride.xcconfig index 2969db2882..fac2d9ba08 100644 --- a/LoopConfigOverride.xcconfig +++ b/LoopConfigOverride.xcconfig @@ -9,6 +9,9 @@ // Customize this to change the URL to open Loop to something other than the display name //URL_SCHEME_NAME = $(MAIN_APP_DISPLAY_NAME) +// Set this to how many days you want to keep data locally on the phone +//LOOP_LOCAL_CACHE_DURATION_DAYS = 90 + // Features SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) EXPERIMENTAL_FEATURES_ENABLED SIMULATORS_ENABLED ALLOW_ALGORITHM_EXPERIMENTS DEBUG_FEATURES_ENABLED diff --git a/LoopKit b/LoopKit index e7e2ee2b54..1e068c68df 160000 --- a/LoopKit +++ b/LoopKit @@ -1 +1 @@ -Subproject commit e7e2ee2b546c4d8122014838cb98a0e26dd91208 +Subproject commit 1e068c68df037e588d850f82e36453e7d835a5e8 diff --git a/LoopOnboarding b/LoopOnboarding index 64f978e143..c7a0b9270e 160000 --- a/LoopOnboarding +++ b/LoopOnboarding @@ -1 +1 @@ -Subproject commit 64f978e143723765452957cef06a99db380b128c +Subproject commit c7a0b9270e733ddb8d085ad41f2ce0683d113195 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/contents.xcworkspacedata b/LoopWorkspace.xcworkspace/contents.xcworkspacedata index fed13e7d24..41ac8e03f8 100644 --- a/LoopWorkspace.xcworkspace/contents.xcworkspacedata +++ b/LoopWorkspace.xcworkspace/contents.xcworkspacedata @@ -78,6 +78,12 @@ + + + + @@ -129,6 +135,9 @@ + + diff --git a/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved index 85b387d04a..06bc80653a 100644 --- a/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7645108625333b4ec60e0e439db0c0dc8a91ad0942d36797c6b66208a9082ea2", + "originHash" : "3074fb13e969eeb07af13d2659671d107eac739de84ccaff57f73a4b6201e10f", "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", @@ -88,7 +97,7 @@ "location" : "https://github.com/mixpanel/mixpanel-swift.git", "state" : { "branch" : "master", - "revision" : "c676a9737c76e127e3ae5776247b226bc6d7652d" + "revision" : "b4cb3f6e3e3084e1637c4dfe06c2dcda169ff523" } }, { @@ -159,7 +168,7 @@ "location" : "https://github.com/tidepool-org/TidepoolKit", "state" : { "branch" : "dev", - "revision" : "54045c2e7d720dcd8a0909037772dcd6f54f0158" + "revision" : "290bd0431c55e8d0e0e542b163de13b1f07b77d9" } }, { @@ -167,7 +176,6 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/LoopKit/ZIPFoundation.git", "state" : { - "branch" : "stream-entry", "revision" : "c67b7509ec82ee2b4b0ab3f97742b94ed9692494" } } diff --git a/LoopWorkspace.xcworkspace/xcshareddata/xcschemes/LoopWorkspace.xcscheme b/LoopWorkspace.xcworkspace/xcshareddata/xcschemes/LoopWorkspace.xcscheme index 19f95ce326..256d8b57df 100644 --- a/LoopWorkspace.xcworkspace/xcshareddata/xcschemes/LoopWorkspace.xcscheme +++ b/LoopWorkspace.xcworkspace/xcshareddata/xcschemes/LoopWorkspace.xcscheme @@ -174,6 +174,34 @@ ReferencedContainer = "container:OmnipodKit/OmnipodKit.xcodeproj"> + + + + + + + + + + + + /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/Scripts/pull-libreloop-log.sh b/Scripts/pull-libreloop-log.sh new file mode 100755 index 0000000000..92f061d10a --- /dev/null +++ b/Scripts/pull-libreloop-log.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Pulls LibreLoop's rolling log file from Pete's iPhone. +# Outputs to /tmp/libreloop-log/. +# +# Requires a dev build of Loop installed (get-task-allow entitlement). +# Honors LIBRELOOP_DEVICE_ID and LIBRELOOP_BUNDLE_ID for override. + +DEVICE="${LIBRELOOP_DEVICE_ID:-4950044E-6D03-564F-A1D9-E86E77D99613}" +BUNDLE="${LIBRELOOP_BUNDLE_ID:-com.UY678SP37Q.loopkit.Loop}" +DEST="${1:-/tmp/libreloop-log}" + +rm -rf "$DEST" +mkdir -p "$DEST" +xcrun devicectl device copy from \ + --device "$DEVICE" \ + --domain-type appDataContainer \ + --domain-identifier "$BUNDLE" \ + --source Documents/libreloop \ + --destination "$DEST" \ + "$@" >/dev/null 2>&1 + +if [ -f "$DEST/libreloop/log.txt" ]; then + echo "Pulled $(wc -l < "$DEST/libreloop/log.txt") lines to $DEST/libreloop/log.txt" + [ -f "$DEST/libreloop/log.1.txt" ] && echo "Plus rotated log $DEST/libreloop/log.1.txt" +else + echo "No log file found at $DEST/libreloop/log.txt" + ls -la "$DEST" 2>/dev/null + exit 1 +fi 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 5253477701..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.2 +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/loop-diy-sync-dev-notes.md b/docs/loop-diy-sync-dev-notes.md new file mode 100644 index 0000000000..53f07c2537 --- /dev/null +++ b/docs/loop-diy-sync-dev-notes.md @@ -0,0 +1,311 @@ +# Loop DIY — Sync & Developer Notes + +*Development-level record of the Tidepool → DIY syncs (2026-03-10 and 2026-05-11), +the merge conflicts and how they were resolved, the DIY divergences that must be +defended on future syncs, data-migration internals, and the DIY-only work that has +landed on `next-dev` since.* + +Last updated: 2026-07-14. + +For the user-facing summary of these changes, see +[What's New](loop-diy-whats-new.md). This document is the "how and why" companion: +conflicts, resolutions, testing, and gotchas. + +--- + +## 1. Sync history & headline numbers + +| Sync | Branch | Scope | +|---|---|---| +| **2026-03-10** | `tidepool-sync/2026-03-10` | The large rebase: LoopAlgorithm package extraction, Swift Concurrency migration, HealthKit → LoopUnit migration, presets overhaul. 18 repos. | +| **2026-05-11** | `tidepool-sync/2026-05-11` | Follow-up absorbing ~2 months of incremental Tidepool work. 17 repos + LoopAlgorithm pin bump. ~660 Tidepool commits total. | + +Commits absorbed in the 2026-05-11 sync (largest first): LoopKit 409, TidepoolService 45, +LoopOnboarding 28, NightscoutService 22, OmniBLE 20, OmniKit/MinimedKit 18 each, +G7SensorKit/dexcom-share-client-swift 15 each, Loop 14, LibreTransmitter 14, +CGMBLEKit/NightscoutRemoteCGM 13 each, LoopSupport 11, AmplitudeService/LogglyService 6 +each, LoopAlgorithm 4 (test-only), RileyLinkKit 3, MixpanelService 0. + +Both syncs originated from PR +[LoopKit/LoopWorkspace#450](https://github.com/LoopKit/LoopWorkspace/pull/450) +(supersedes #213). The original per-doc files this consolidates: +`docs/tidepool-sync-2026-03-10.md` and `docs/tidepool-sync-2026-05-11.md`. + +--- + +## 2. LoopAlgorithm architecture change + +**The single most important architectural fact.** DIY previously embedded algorithm +logic inline inside `LoopKit` (`LoopKit/LoopKit/LoopAlgorithm/`), with effects computed +across `CarbStore` / `DoseStore` / `GlucoseStore` and orchestrated by `LoopDataManager`. +`LoopAlgorithm` was a stateful `actor`. + +It is now the **`tidepool-org/LoopAlgorithm` Swift package**, reconceived as a **pure +function**: + +``` +AlgorithmOutput = LoopAlgorithm.run(input: AlgorithmInput) // caseless enum, static, no state +``` + +| Aspect | Before (DIY inline) | After (package) | +|---|---|---| +| Type | `public actor` (stateful, async) | `public enum` (static methods only) | +| Entry point | `generatePrediction(input:startDate:) throws` | `run(input:) -> AlgorithmOutput` (non-throwing; errors in `Result`) | +| Effects | Distributed across stores | All in `LoopAlgorithm` static methods, exposed as 8 fields | +| Error cases | 2 | 7 (granular) | +| Units | `HKUnit` / `HKQuantity` | `LoopUnit` / `LoopQuantity` (no HealthKit) | +| Dependencies | HealthKit, CoreData | None (pure Swift) | +| Testing | Mocked async stores | Pure value types + JSON fixtures | + +> ⚠️ **Packaging decision was reversed between the two syncs.** The 2026-03-10 doc +> originally said DIY keeps LoopAlgorithm *inline* and *omits* the +> `XCRemoteSwiftPackageReference "LoopAlgorithm"`. **This is no longer true.** As of the +> 2026-05-11 sync (LOOP-4781), DIY consumes the `tidepool-org/LoopAlgorithm` **Swift +> package**, pinned in the workspace `Package.resolved`, and the inline copies +> (`LoopAlgorithm.swift`, `LoopPredictionOutput.swift`, `ExponentialInsulinModel.swift`) +> were **deleted**. `import LoopAlgorithm` is present again everywhere. **If `AGENTS.md` +> still says "keep LoopAlgorithm inline / omit the package reference," that guidance is +> stale and should be reconciled against this section.** + +Algorithm-level improvements included in the synced package version: gradual effect +transitions (PR #23), configurable `maxActiveInsulinMultiplier` default 2× (LOOP-5502), +runtime `carbAbsorptionModel` selection (PR #21), `AutomaticDoseRecommendation` backward +decode fix (PR #20), mid-absorption ISF fix (PR #19), `TempBasalRecommendation.direction` +(LOOP-5295), Swift 6 `Sendable` conformances (PR #14), glucose-math tests moved into the +package (PR #24). + +--- + +## 3. Merge conflicts & resolutions + +### 3.1 Conflict volume by repo + +| Repo | 2026-03-10 | 2026-05-11 | +|---|---|---| +| LoopAlgorithm | none (clean FF, 29 ahead) | pin bump only (4 test-only) | +| LoopKit | 16 (source + pbxproj) | 18 source + 19 pbxproj regions | +| Loop | 33 across 30+ files | 3 pbxproj regions only (no source) | +| Peripheral repos (9) | pbxproj-only | 11/15 plugins: only the `LOCALIZATION_PREFERS_STRING_CATALOGS` line | +| Plugin source conflicts | 6 repos | 5 repos | + +### 3.2 Loop — the deepest conflict: `LoopDataManager.swift` + +7 conflict hunks from a fundamental divergence: Tidepool migrated to Swift Concurrency +(`@MainActor async/await`, `Task {}`); DIY had `dataAccessQueue`-based threading plus +Live Activity integration. + +**Resolution:** adopt Tidepool's `Task { @MainActor in await updateDisplayState() }` +model throughout; **remove** DIY's `dataAccessQueue`, `lockedSettings`, `mutateSettings()`, +`loop()`, `loopInternal()`, `finishLoop()` chain; inject DIY's Live Activity calls into +Tidepool's new `updateDisplayState()` call sites; adopt new init params +(`analyticsServicesManager`, `carbAbsorptionModel`, +`usePositiveMomentumAndRCForManualBoluses`, `dosingStrategySelectionEnabled`) and move all +stored-property assignments before the `overrideIntentObserver` closure. + +### 3.3 Loop — file-level resolutions (2026-03-10) + +| File | Resolution | +|---|---| +| `SystemActionLink.swift` | Deleted; replaced by Tidepool's `DeeplinkView.swift`. Port DIY widget tinting if verified missing. | +| `FavoriteFoodDetailView.swift` | Accept Tidepool's move to `Views/Favorite Foods/`. | +| `Main.strings` | Keep deleted — DIY is on `.xcstrings`. | +| `AppDelegate.swift` | Keep both (DIY logging + Tidepool XCTest guard). | +| `DeviceDataManager.swift` | Keep both (DIY diagnostic report + Tidepool `roundBasalRate`). | +| `SettingsView.swift` | Keep both sheets (Favorite Foods + Presets). | +| `NSUserDefaults.swift` | Keep both keys (`liveActivity` + `defaultEnvironment`). | +| `WidgetBackground.swift` | Take Tidepool's `Color.widgetBackground` extension. | +| `ContentMargin.swift` | Keep DIY (only copyright differed). | + +### 3.4 Plugin source conflicts (2026-05-11) + +| Repo / File | Resolution | +|---|---| +| OmniKit / `OmnipodPumpManager.swift` | Keep DIY `isSignalLost(...)` reentrant-lock fix (`924f10d`); take Tidepool `setState` → `mutateState`. | +| OmniBLE / `OmniBLEPumpManager.swift` | Keep DIY reentrant-lock fix (`e9425ad`) + `completion(.communication(error))` style; **manual merge** at line 2722 to preserve DIY suspend-time-expired special case (Pod Keep Alive #165) while adopting Tidepool's `withCheckedThrowingContinuation`; take Tidepool `mutateState`. | +| MinimedKit / `MinimedPumpManager.swift` | Keep DIY all 3 regions — preserves CAGE/IAGE tracking (`ff07802`); dropped Tidepool's duplicate `isInoperable`. | +| TidepoolService / `DoseEntry.swift` | Deduped duplicate `import LoopAlgorithm` (`5f6a064`). | +| NightscoutService / `NightscoutService.swift` | Keep DIY — preserves the 60-line APNS response feature (`RemoteNotificationResponseManager`, `0ca2c08`). | + +### 3.5 Accepted Tidepool deletions + +- Inline algorithm copies (LOOP-4781): `ExponentialInsulinModel.swift`, + `LoopAlgorithm/LoopAlgorithm.swift`, `LoopAlgorithm/LoopPredictionOutput.swift`. +- `LoopKitUI/View Controllers/OverrideSelectionViewController.swift` — superseded by the + new SwiftUI `EditPresetView` / `ReviewNewPresetView`; DIY crash-fix `3ce43ded` doesn't + apply to the SwiftUI flow. + +### 3.6 pbxproj merge rules (applied across all repos) + +| Setting | Decision | +|---|---| +| `IPHONEOS_DEPLOYMENT_TARGET` | Take the higher value. | +| `LOCALIZATION_PREFERS_STRING_CATALOGS` | Keep `YES`. | +| `.strings` file references | Drop Tidepool's (DIY deleted the files; re-adding breaks the build). | +| `.xcstrings` references | Keep DIY's. | +| `XCRemoteSwiftPackageReference "LoopAlgorithm"` | **Keep** (post-4781; the pre-existing HEAD reference was left alone). | +| New Swift file references | Keep both additively, then dedup `children`/`files`. | +| Bundle IDs | Keep both (different targets). | + +**Structural rule:** "keep both" is *wrong* across a `PBXVariantGroup` / `PBXGroup` +boundary. Always check `brace_count(ours) + brace_count(theirs)` before keeping both; if +non-zero, take OURS. After any pbxproj merge, verify brace depth == 0 and validate with +`xcodebuild -project .xcodeproj -list`. (Loop's conflict 36 produced a +2 depth and +had to be re-resolved; 7 duplicate group children were removed.) + +--- + +## 4. DIY divergences (defend these on every future sync) + +These are points where DIY intentionally rejects a Tidepool change. Auto-merge has +silently reverted some of them before — each carries an inline defending comment. + +| Divergence | Repo | Why | +|---|---|---| +| **BasalRateScheduleEditor max-basal filtering** | LoopKit | DIY rejects Tidepool PR #734 (LOOP-5767) which passed `maximumBasalRate: nil` and let users set basal entries above their configured max. Restored `maximumBasalRatePerHour`; see `memory/divergence_basal_max_filter.md`. Silently reverted on 2026-05-11; manually restored. | +| **slot6SuspendTimeExpired guard** | OmniBLE | Safety-critical: a suspended pod must keep beeping; don't silently ack. Not present in OmniKit (different pod hardware). | +| **updateLastEventDates** | MinimedKit | Cannula-age / insulin-age tracking; no Tidepool equivalent. | +| **RemoteNotificationResponseManager** | NightscoutService | Remote-command feedback (remote bolus/basal); core DIY feature. | +| **Mock debug settings views** | LoopKit (MockKitUI) | `MockCGMManagerSettingsView` / `MockPumpManagerSettingsView` gated by `allowDebugFeatures`. | +| **Reentrant-lock fixes** | OmniKit / OmniBLE | `isSignalLost` reentrancy (`924f10d` / `e9425ad`). | +| **`.xcstrings` localization** | all | DIY uses Xcode 15+ string catalogs; do **not** re-add `.strings` refs. Tidepool doesn't maintain translations. | + +--- + +## 5. Post-merge fixes (2026-03-10) + +- **`LoopDataManager.swift`** — moved all stored-property assignments before the + `overrideIntentObserver` closure (Swift init-before-capture). +- **`NSUserDefaults.swift`** — restored two missing closing braces in the `liveActivity` + computed property. +- **`TidepoolServiceKit/Extensions/DoseEntry.swift`, `DeviceLogUploader.swift`** — restored + `import LoopAlgorithm` (removed in error during conflict resolution; only CLI builds + masked it via implicit module visibility). +- **`Loop.xcodeproj/project.pbxproj`** — re-resolved the `PBXVariantGroup` conflict that + produced +2 brace depth; deduped 7 group children; validated with `xcodebuild -list`. + +--- + +## 6. Core Data v4 → v6 migration & downgrade caveat + +The shared LoopKit store (`Model.sqlite`, app group) migrates **Modelv4 → Modelv6** on +first launch of a sync build. `dev` only ships up to Modelv4. + +- **Forward migration preserves insulin data.** `CachedInsulinDeliveryObjectMigrationPolicy` + copies `value` → `deliveredUnits` (and `programmedUnits` for boluses); basal *rates* + carry over via `scheduledBasalRate` / `programmedTempBasalRate`. Without this policy the + v4→v6 mapping dropped the single `value` attribute and zeroed cached bolus/basal amounts + (understating IOB). **Only the forward path is fixed** — installs that already migrated on + a policy-less build have already-dropped values that read as 0 and can't be recovered. +- **Downgrading back to `dev` is data-dependent.** `dev` *can* open a v6 store via + lightweight migration **immediately after upgrading** (v6-only attributes are optional + and dropped; `deliveredUnits` shares the `ZVALUE` column with v4's `value` via + `elementID="value"`). **But** `dev`'s `CachedInsulinDeliveryObject.value` is *mandatory* + while v6's `deliveredUnits` is *optional* — once the sync build records a dose with a + **null `deliveredUnits`** (in-progress/mutable or programmed-only), the downgrade throws + `NSCocoaErrorDomain 134110` ("missing attribute values on mandatory destination + attribute"), `addPersistentStore` fails, `PersistenceController` lands in `.error`, and + `dev` is non-functional until delete + reinstall (which rebuilds the cache from HealthKit). + The v6 data on disk is never wiped, so reinstalling the sync build always reads it again. + +**Practical guidance:** in-place downgrade to `dev` is fine right after upgrading, gets +unreliable the longer the sync build runs. Don't rely on it for round-tripping. + +--- + +## 7. Post-sync development on `next-dev` (since ~2026-05-20) + +DIY-only work that landed after the two syncs. All on `LoopKit/Loop next-dev` unless +noted; workspace gitlinks bumped on `LoopKit/LoopWorkspace next-dev`. + +### 7.1 Device driver re-organization + +- **OmniKit + OmniBLE consolidated into `OmnipodKit`** (loop-and-learn's combined pod + driver; wired via Loop #2426 `1f71ec4f`). OmniKit/OmniBLE are no longer separate + workspace submodules. +- **`MedtrumKit`** added (Medtrum Nano / TouchCare pump). Fork: `LoopKit/MedtrumKit` off + `jbr7rr/MedtrumKit`. +- **`EversenseKit`** added (Senseonics Eversense CGM); deployment target set to 17.0. +- **`LibreLoop` + `LibreCRKit`** added (native FreeStyle Libre 3). Substantial DIY work: + BLE pairing/stability, reconnect backoff + re-scan prompt, backfill of missed readings + (Libre 3 CCCD/data-plane arming — the `0xFD` fix, full-response-set re-arm), 5-minute + backfill grid thinning, sensor-failure UI, mg/dL vs mmol/L display, module-scoped + localization (String Catalogs), and a Glucose Streams debug view. `LibreCRKit` tracks + upstream `airedev326/LibreCRKit` — contribute via patch/PR, bump the pin when merged. + +### 7.2 New user-facing features (see What's New for the user framing) + +- **Statistics / AGP page** — new `GlucoseStatistics` model + `StatisticsView` / + `AGPChartView` / `TimeInRangeBar` (Apple Swift Charts). Reads the 90-day glucose cache + (`LOOP_LOCAL_CACHE_DURATION_DAYS = 90`). Per-zone percentile band painting; captūrAGP/IDC + coloring; 7/14/30/90-day windows with an incomplete-data notice. +- **Glucose Alerts system** — `GlucoseAlertManager` + `GlucoseAlertSettingsView`. Per-profile + Urgent Low / Low / High / Predicted Low thresholds; selectable alarm sounds; "Delay 1st + Alert" for highs; issues only the most severe low-side alert; suppresses predicted-low when + already at/below Low. **Thresholds stored canonically in mg/dL; display/entry convert to the + user's unit** via `DisplayGlucosePreference` + `GlucoseValuePicker` (`a325b943`). +- **AlarmKit critical alerts (iOS 26+)** — critical alerts without the entitlement; fires an + alarm at a near-future date with an audio fallback (`critical.caf`, MPVolumeView volume + boost); Stop acknowledges the Loop alert; in-app Alerts FAQ for no-entitlement builds. +- **Watch** — fetch glucose on foreground; backfill from latest stored sample. +- **Manual temp basal** — pause looping while running; surface in `LoopStatusModalView` HUD. +- **Display forecast** — project ongoing suspends / temp basals into the forecast. +- **De-branding** — replace hardcoded "Tidepool Loop" with the host app name; Focus-mode + imagesets; app version moved into the Support section. +- **Carb entry** honors `LoopConstants` for date-picker limits (`5f0686cf`). + +### 7.3 Test / CI / build infrastructure + +- Test fixes for the current LoopAlgorithm + DIY behavior: `LoopDataManagerTests`, + `LoopKitTests` (guardrails 87/67, continuous decay, sorted `appendedUnion` input), + full-scheme test isolation (`testOpenLoopCancelsTempBasal`), + `isManualTempBasalRunning` on `MockDeliveryDelegate`. +- **CI moved to Xcode 26.4** (from 16.4), iPhone 17 simulator, dropped the simulator OS pin; + `xcbeautify` instead of `xcpretty` so test failures surface. +- **Known flake:** `SettingsManagerTests` intermittently hangs (~23s timeout → retry → + "Executed 0 tests"), producing sporadic red on otherwise-identical code. Re-run when seen. +- OmnipodKit CryptoSwift 1.9+ compatibility (`Data.bytes` → `Array(data)`); + LoopOnboarding links `NightscoutServiceKitUI` to fix an explicit-modules build race. + +--- + +## 8. Testing checklist (consolidated) + +### Algorithm & dosing +- [ ] Glucose predictions; mid-absorption ISF (dose effect follows the ISF *timeline* as + insulin absorbs, not a single ISF fixed at dose time); max-IOB multiplier (2×). +- [ ] `TempBasalRecommendation.direction` populated. +- [ ] Display forecast projects ongoing suspends / manual temp basals. +- [ ] Pump decision IDs on OmnipodKit / MinimedKit dose entries. + +### Presets +- [ ] All 4 activity presets appear with correct defaults; "modified" indicator works. +- [ ] High-insulin-needs mitigation clamps correction range ≥ 110 mg/dL above ~165%. +- [ ] Indefinite-preset 24-hour reminder fires and retracts on deactivation. +- [ ] Scheduled presets alert at the correct time; "Yes, Start Now" activates. +- [ ] Pre-meal guardrail: 130 mg/dL hard cap; recommended upper bound = correction-range lower bound. +- [ ] Training gates the "+" create button; active-preset banner shows on CarbEntryView. + +### Glucose alerts (DIY) +- [ ] Thresholds display and the picker steps correctly in **both** mg/dL and mmol/L. +- [ ] Only the most severe low-side alert fires; predicted-low suppressed at/below Low. +- [ ] Selectable sounds play; "Delay 1st Alert" honored for highs. +- [ ] AlarmKit critical alerts break through silent/Focus on iOS 26; Stop acks the Loop alert. + +### Statistics +- [ ] AGP bands/median render; GMI/avg/CV/TIR populate; range selector reloads (7/14/30/90); + incomplete-data notice shows; units follow the mg/dL ↔ mmol/L preference. + +### Devices +- [ ] Add Pump lists Omnipod + Medtrum; Add CGM lists Eversense + Libre 3 (LibreLoop) + Dexcom. +- [ ] Libre 3 pairing, reconnect, and backfill (5-min grid) work; no `0xFD` on backfill. +- [ ] OmniBLE temp-basal error reporting surfaces; suspended-pod keeps beeping (slot6 guard). +- [ ] Minimed cannula/insulin age displays (updateLastEventDates). + +### Cross-cutting +- [ ] Live Activity updates on glucose/dosing/carb changes; widget deeplink buttons + colors. +- [ ] Both Favorite Foods and Presets sheets accessible; bolus entry safe-area layout correct. +- [ ] Nightscout remote bolus/basal feedback notifications flow (RemoteNotificationResponseManager). +- [ ] Basal schedule editor (OmniBLE/Dash): entries above configured max are **not** selectable. +- [ ] Diagnostic support report includes submodule SHAs. +- [ ] Unit suites pass: `LoopAlgorithmTests`, `LoopKitTests`, `LoopTests`. +- [ ] Core Data v4→v6 migration preserves insulin amounts; downgrade caveat understood (§6). diff --git a/docs/loop-diy-whats-new.md b/docs/loop-diy-whats-new.md new file mode 100644 index 0000000000..ec4c6d1894 --- /dev/null +++ b/docs/loop-diy-whats-new.md @@ -0,0 +1,225 @@ +# Loop DIY — What's New + +*User-facing guide to the changes on the `next-dev` line, from the two Tidepool → DIY +syncs (2026-03-10 and 2026-05-11) plus everything DIY has added since.* + +Last updated: 2026-07-14. + +This document explains **what changed for you as a user**. For merge mechanics, +conflict resolutions, testing checklists, and data-migration internals, see the +companion [Developer / Sync Notes](loop-diy-sync-dev-notes.md). + +--- + +## The short version + +DIY Loop absorbed roughly two years of Tidepool's work on Loop, then kept going with +DIY-only features on top. The headline user-visible changes: + +- **Presets are completely redesigned** — typed presets (Pre-Meal / Activity / Custom), + evidence-based activity presets, day-of-week scheduling, safety guardrails, and a + short required training before you can create custom presets. +- **A new Glucose Alerts system** — your own high/low/urgent-low/predicted-low + thresholds with selectable alert sounds, shown and edited in **your** glucose unit + (mg/dL *or* mmol/L), plus true critical alerts on iOS 26 without a special entitlement. +- **A Statistics page** — a standard Ambulatory Glucose Profile (AGP) report with + time-in-range, GMI, and glucose variability over 7–90 days. +- **More devices** — Omnipod, Medtrum, Eversense, and FreeStyle Libre 3 are all + supported alongside the existing pumps and CGMs. +- **A smarter forecast and a cleaner algorithm** under the hood. +- **A redesigned main screen**, plus Apple Watch, HUD, and history improvements. + +--- + +## Presets — a full redesign + +This is the biggest change you'll notice day-to-day. + +### Preset types + +Presets are now one of three kinds, each with its own abilities: + +| | Pre-Meal | Activity | Custom | +|---|---|---|---| +| Adjust correction range | ✅ | ✅ | ✅ | +| Adjust insulin needs | — | ✅ | ✅ | +| Set a duration | ends when carbs entered | ✅ | ✅ | +| Run indefinitely | — | — | ✅ | +| Rename / delete | — | — | ✅ | +| Schedule by day & time | — | ✅ | ✅ | + +### Activity presets with evidence-based defaults + +Four ready-made activity presets ship for everyone (customizable, not deletable): + +| Activity | Target range | 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 | + +"Insulin needs" is a single control that scales basal, carb ratio, and ISF together — +so *"give me 21% of normal insulin"* is one number instead of three. If you change a +preset from its defaults, it's marked as **modified**. + +### New safety behavior + +- **High insulin-needs mitigation** — if you push a preset's insulin needs above ~165%, + the correction target is automatically floored at **110 mg/dL**. This prevents the + dangerous combination of a lot of insulin *and* a very low target. The app shows you + when this is in effect. +- **Guardrails while editing** — settings outside the recommended range are flagged in + yellow, and outside the safe range in red, before you can save. +- **Indefinite-preset reminder** — a custom preset with no end time reminds you every + 24 hours that it's still running, so you don't leave one on by accident. + +### Scheduling + +Presets can be scheduled to start at a specific time, optionally repeating on any +combination of days of the week. When a scheduled start time arrives, a time-sensitive +alert asks whether to start it ("Yes, Start Now" / "Don't Start"). + +### Required training + +Before creating your first custom preset, you complete a short in-app training covering +how presets work, illness, daily activities, and exercise. You can review it any time +from the Presets screen. + +### The Presets screen + +A dedicated full-screen view with an active-preset card (showing expected end time), +a sortable list of all presets, and a **Performance History** view of past activations. +While entering carbs, a banner now shows any active preset. + +--- + +## Glucose Alerts (DIY, new since the sync) + +A new glucose-alert system, styled after standalone-receiver alerts: + +- **Your own thresholds** for **Urgent Low**, **Low**, **High**, and **Predicted Low**. +- **Displayed and edited in your glucose unit** — mg/dL or mmol/L, matching every other + screen. The picker steps by 1 mg/dL or 0.1 mmol/L as appropriate. +- **Selectable alert sounds** per alert, and a **"Delay 1st Alert"** option for highs so + you aren't nagged the moment you cross the line. +- **Only the most severe low-side alert fires** — you won't get a Low and an Urgent Low + buzzing at the same time, and a predicted-low alert is suppressed if you're already + at or below your Low threshold. + +### Critical alerts on iOS 26 without an entitlement + +DIY builds don't have Apple's official critical-alert entitlement. On **iOS 26 and +later**, Loop now uses Apple's **AlarmKit** to deliver true critical alerts that break +through silent mode and Focus — no entitlement required. When it can't, it falls back to +boosting system volume and playing a bundled critical sound. Tapping **Stop** on the +alarm acknowledges the underlying Loop alert. Builds without the entitlement include an +in-app FAQ explaining how alerts behave. + +--- + +## Statistics — Ambulatory Glucose Profile (DIY, new since the sync) + +**Settings → Statistics** opens a standard **AGP report**: + +- Time-in-range across the consensus bands (very low / low / in-range / high / very high). +- **GMI** (Glucose Management Indicator), **average glucose**, and **glucose variability** + (coefficient of variation). +- The AGP percentile curves (median with 25–75% and 5–95% bands) by time of day, colored + to match the clinical captūrAGP / IDC report style. +- Selectable window of **7, 14, 30, or 90 days**, with a notice when a window doesn't have + complete data. + +This is powered by a longer on-device glucose history (now kept for **90 days**), so the +report has enough data to be meaningful without waiting on slow HealthKit queries. + +--- + +## More devices + +Device support expanded well beyond the original sync set. + +### Pumps +- **Omnipod** (Eros, DASH, and **Omnipod 5**) — now via the consolidated **OmnipodKit** + driver, which adds Omnipod 5 support. +- **Medtrum** (Nano / TouchCare) — **new**. +- **Medtronic (Minimed)** — retained, including DIY's cannula-age / insulin-age tracking. + +### CGMs +- **FreeStyle Libre 3** — **new**, via the native **LibreLoop** integration (direct BLE + pairing, backfill of missed readings, and sensor-failure guidance UI). Readings and + settings respect your mg/dL vs mmol/L preference. +- **Eversense** (Senseonics E3 / 365) — **new**, via **EversenseKit**. +- **Dexcom G6/G7**, **older Libre (via LibreTransmitter)**, and **Nightscout-as-CGM** — + all retained. + +Every pump command is now tagged with the algorithm **decision** that produced it, which +improves troubleshooting and the accuracy of what's uploaded to Nightscout/Tidepool. + +--- + +## Under the hood: a cleaner, smarter algorithm + +The dosing math moved into a dedicated **LoopAlgorithm** package. For you, that mostly +means faster, more predictable behavior and some concrete improvements Tidepool made: + +- **Smoother forecasts** — insulin and carb effects now ramp up gradually at the start of + absorption instead of jumping to full effect. +- **Selectable carb-absorption model** (parabolic / linear / piecewise-linear). +- **A max-active-insulin cap** expressed as a multiple of your max bolus (default 2×). +- **Insulin dose effects now follow your ISF schedule as the dose absorbs** — a bolus or + basal keeps acting for hours. Previously Loop valued that whole absorption with a single + ISF: the one in effect at the moment the dose was delivered. Now it uses your insulin + sensitivity *timeline*, so if your ISF changes while an earlier dose is still absorbing, + the remaining effect of that dose is recalculated at the new sensitivity — more accurate + insulin-effect and IOB accounting across ISF changes. +- **A "gradual transitions" guard against noisy CGM data** — if your glucose readings jump + sharply from one reading to the next (more than ~40 mg/dL between consecutive readings), + the algorithm skips **glucose momentum and retrospective correction** for that cycle. + Those two effects extrapolate your recent short-term trend, so on a big jump — a sensor + glitch, a compression low, or a one-off spike — they'd amplify the noise instead of + reflecting a real trend. Skipping them keeps dosing steadier through jumpy data. +- **The display forecast now projects ongoing suspends and manual temp basals**, so the + prediction curve reflects what the pump is actually doing. + +--- + +## Main screen, Watch, HUD, and everyday UI + +### A redesigned main screen +- **A single insulin delivery chart** replaces the previous two-chart layout (a separate + delivery chart and Active Insulin chart), for a cleaner status view. +- **Time since last loop** is shown, so you can tell at a glance how recently Loop ran. +- **An updated loop-status dialog** appears when you tap the loop-status icon, with clearer + detail about the current loop state. +- **A redesigned Insulin Delivery Log** — tapping the Active Insulin chart opens a new log + showing the day's total insulin delivery, with a filter to show *all* delivery or just + your *user-initiated* doses (hiding automated temp basals). Tap any dose to see its + details, including the dosing decision that produced it, and log a manual (non-pump) dose + from the same screen. (When no pump is set up, the classic event-history screen is shown + instead, since the new log relies on live pump state.) + +### Watch, HUD, and history +- **Watch** refreshes glucose the moment you raise your wrist / open the app, and backfills + from the latest stored reading. +- **Manual temp basal** now pauses looping while it runs and is clearly surfaced in the + status HUD, so it's obvious when the loop is intentionally paused. +- **Longer history** — the app keeps 90 days of glucose locally (up from a shorter window), + feeding the new Statistics page. +- **App version** moved into the Settings → Support section. +- **Clearer alert-permission warnings** — instead of one generic "notifications off" + message, the app tells you specifically which permission (notifications, critical + alerts, or time-sensitive) is disabled. +- **Required app-update flow** — the app can prompt you when a mandatory update is needed. + +--- + +## If you're moving between DIY `dev` and this build + +Upgrading to a `next-dev` build migrates your local data store forward automatically. +**Going back to a `dev` build in place is not guaranteed** — it works right after +upgrading, but becomes unreliable the longer you run the newer build, and can leave `dev` +unable to open the store (requiring a delete-and-reinstall). Your long-term history lives +in Apple Health and is rebuilt on reinstall, but **if you plan to switch back and forth, +don't rely on an in-place downgrade.** See the Developer / Sync Notes for the exact +mechanism. 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`