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/.github/workflows/build_loop.yml b/.github/workflows/build_loop.yml index 9dab27ff57..7bf98a8de7 100644 --- a/.github/workflows/build_loop.yml +++ b/.github/workflows/build_loop.yml @@ -1,11 +1,7 @@ -name: 4. Build Loop -run-name: Build Loop (${{ github.ref_name }}) +name: 4. Build Loop Manual +run-name: Build Loop Manual (${{ github.ref_name }}) on: workflow_dispatch: - schedule: - # Check for updates every Sunday - # Later logic builds if there are updates or if it is the 2nd Sunday of the month - - cron: "33 7 * * 0" # Sunday at UTC 7:33 env: GH_PAT: ${{ secrets.GH_PAT }} @@ -260,4 +256,4 @@ jobs: name: build-artifacts path: | artifacts - buildlog + buildlog \ No newline at end of file diff --git a/.github/workflows/build_loop_auto.yml b/.github/workflows/build_loop_auto.yml new file mode 100644 index 0000000000..5d2d647be3 --- /dev/null +++ b/.github/workflows/build_loop_auto.yml @@ -0,0 +1,287 @@ +name: 4. Build Loop Auto +run-name: Build Loop Auto (${{ github.ref_name }}) +on: + workflow_dispatch: + schedule: + # Check for updates every Sunday + # Later logic builds if there are updates or if it is the 2nd Sunday of the month + - cron: "33 7 * * 0" # Sunday at UTC 7:33 + +env: + GH_PAT: ${{ secrets.GH_PAT }} + UPSTREAM_REPO: LoopKit/LoopWorkspace + UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed) + TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync + +jobs: + # use a single runner for these sequential steps + check_status: + runs-on: ubuntu-latest + name: Check status to decide whether to build + permissions: + contents: write + outputs: + NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }} + IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }} + + # Check GH_PAT, sync repository, check day in month + steps: + + - name: Access + id: workflow-permission + run: | + # Validate Access Token + + # Ensure that gh exit codes are handled when output is piped. + set -o pipefail + + # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens. + GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$' + GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$' + + # Validate Access Token (GH_PAT) + if [ -z "$GH_PAT" ]; then + failed=true + echo "::error::The GH_PAT secret is unset or empty. Set it and try again." + else + if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then + provides_scopes=true + echo "The GH_PAT secret is a structurally valid classic token." + elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then + echo "The GH_PAT secret is a structurally valid fine-grained token." + else + unknown_format=true + echo "The GH_PAT secret does not have a known token format." + fi + + # Attempt to capture the x-oauth-scopes scopes of the token. + if ! scopes=$(curl -sS -f -I -H "Authorization: token $GH_PAT" https://api.github.com | { grep -i '^x-oauth-scopes:' || true; } | cut -d ' ' -f2- | tr -d '\r'); then + failed=true + if [ $unknown_format ]; then + echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that it is set correctly (including the 'ghp_' or 'github_pat_' prefix) and try again." + else + echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that the token exists and has not expired at https://github.com/settings/tokens. If necessary, regenerate or create a new token (and update the secret), then try again." + fi + elif [[ $scopes =~ workflow ]]; then + echo "The GH_PAT secret has repo and workflow permissions." + echo "has_permission=true" >> $GITHUB_OUTPUT + elif [[ $scopes =~ repo ]]; then + echo "The GH_PAT secret has repo (but not workflow) permissions." + elif [ $provides_scopes ]; then + failed=true + if [ -z "$scopes" ]; then + echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes." + else + echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes" + fi + echo "::error::The GH_PAT secret is lacking at least the 'repo' permission scope required to access the Match-Secrets repository. Update the token permissions at https://github.com/settings/tokens (to include the 'repo' and 'workflow' scopes) and try again." + else + echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide inspectable scopes. Assuming that the 'repo' and 'workflow' permission scopes required to access the Match-Secrets repository and perform automations are present." + echo "has_permission=true" >> $GITHUB_OUTPUT + fi + fi + + # Exit unsuccessfully if secret validation failed. + if [ $failed ]; then + exit 2 + fi + + - name: Checkout target repo + if: | + steps.workflow-permission.outputs.has_permission == 'true' && + (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') + uses: actions/checkout@v5 + with: + token: ${{ secrets.GH_PAT }} + + # This syncs any target branch to upstream branch of the same name + - name: Sync upstream changes + if: | # do not run the upstream sync action on the upstream repository + steps.workflow-permission.outputs.has_permission == 'true' && + vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit' + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2 + with: + target_sync_branch: ${{ env.TARGET_BRANCH }} + shallow_since: 6 months ago + target_repo_token: ${{ secrets.GH_PAT }} + upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }} + upstream_sync_repo: ${{ env.UPSTREAM_REPO }} + + # Display a sample message based on the sync output var 'has_new_commits' + - name: New commits found + if: | + steps.workflow-permission.outputs.has_permission == 'true' && + vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' + run: echo "New commits were found to sync." + + - name: No new commits + if: | + steps.workflow-permission.outputs.has_permission == 'true' && + vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' + run: echo "There were no new commits." + + - name: Show value of 'has_new_commits' + if: steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' + run: | + echo ${{ steps.sync.outputs.has_new_commits }} + echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT + + - name: Show scheduled build configuration message + if: steps.workflow-permission.outputs.has_permission != 'true' + run: | + echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY + echo "You have not yet configured the scheduled sync and build for Loop's browser build." >> $GITHUB_STEP_SUMMARY + echo "Synchronizing your fork of LoopWorkspace with the upstream repository LoopKit/LoopWorkspace will be skipped." >> $GITHUB_STEP_SUMMARY + echo "If you want to enable automatic builds and updates for your Loop, please follow the instructions \ + under the following path LoopWorkspace/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY + + # Set a logic flag if this is the second instance of this day-of-week in this month + - name: Check if this is the second time this day-of-week happens this month + id: date-check + run: | + DAY_OF_MONTH=$(date +%-d) + WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 )) + if [[ $WEEK_OF_MONTH -eq 2 ]]; then + echo "is_second_instance=true" >> "$GITHUB_OUTPUT" + else + echo "is_second_instance=false" >> "$GITHUB_OUTPUT" + fi + + # When a scheduled run finds nothing new to build, cancel the run so it shows + # as "cancelled" instead of "success" — a green check should always mean a + # new build was made. + cancel_when_no_build: + needs: check_status + name: Cancel run when there is nothing to build + runs-on: ubuntu-latest + permissions: + actions: write + if: | + github.event_name == 'schedule' && + !(vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') && + !(vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true') + steps: + - name: Cancel the run + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Repository is up to date and no monthly build is due — cancelling this run so it is not reported as a successful build." >> "$GITHUB_STEP_SUMMARY" + gh run cancel ${{ github.run_id }} --repo ${{ github.repository }} + # Keep the job alive until the cancellation lands, so the run ends as + # "cancelled" rather than completing successfully first. + sleep 300 + + # Checks if Distribution certificate is present and valid, optionally nukes and + # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true' + # only run if a build is planned + check_certs: + needs: [check_status] + name: Check certificates + uses: ./.github/workflows/create_certs.yml + secrets: inherit + if: | + github.event_name == 'workflow_dispatch' || + (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') || + (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' ) + + # Builds Loop + build: + name: Build + needs: [check_certs, check_status] + runs-on: macos-26 + permissions: + contents: write + if: + | # builds with manual start; if scheduled: once a month or when new commits are found + github.event_name == 'workflow_dispatch' || + (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') || + (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' ) + steps: + - name: Select Xcode version + run: "sudo xcode-select --switch /Applications/Xcode_26.5.app/Contents/Developer" + + - name: Checkout Repo for building + uses: actions/checkout@v5 + with: + token: ${{ secrets.GH_PAT }} + submodules: recursive + ref: ${{ env.TARGET_BRANCH }} + + # Customize Loop: Download and apply patches + - name: Customize Loop + run: | + + # LoopWorkspace patches + # -applies any patches located in the LoopWorkspace/patches/ directory + if $(ls ./patches/* &> /dev/null); then + git apply ./patches/* --allow-empty -v --whitespace=fix + fi + + # Submodule Loop patches: + # Template for customizing submodule Loop (changes Loop app name to "CustomLoop") + # Remove the "#" sign from the beginning of the line below to activate: + #curl https://github.com/loopnlearn/Loop/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply --directory=Loop -v --whitespace=fix + + # Submodule LoopKit patches: + # General template for customizing submodule LoopKit + # Copy url from a GitHub commit or pull request and insert below, and remove the "#" sign from the beginning of the line to activate: + #curl url_to_github_commit.patch | git apply --directory=LoopKit -v --whitespace=fix + + # Submodule xxxxx patches: + + # Add patches for customization of additional submodules by following the templates above, + # and make sure to specify the submodule by setting "--directory=(submodule_name)". + # Several patches may be added per submodule. + # Adding comments (#) may be useful to easily tell the individual patches apart. + + # Patch Fastlane Match to not print tables + - name: Patch Match Tables + run: | + TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb + if [ -f "$TABLE_PRINTER_PATH" ]; then + sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH" + else + echo "table_printer.rb not found" + exit 1 + fi + + # Install project dependencies + - name: Install Project Dependencies + run: bundle install + + # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) + - name: Sync clock + run: sudo sntp -sS time.windows.com + + # Build signed Loop IPA file + - name: Fastlane Build & Archive + run: bundle exec fastlane build_loop + env: + TEAMID: ${{ secrets.TEAMID }} + GH_PAT: ${{ secrets.GH_PAT }} + FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} + FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} + FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + + # Upload to TestFlight + - name: Fastlane upload to TestFlight + run: bundle exec fastlane release + env: + TEAMID: ${{ secrets.TEAMID }} + GH_PAT: ${{ secrets.GH_PAT }} + FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} + FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} + FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + + # Upload Build artifacts + - name: Upload build log, IPA and Symbol artifacts + if: always() + uses: actions/upload-artifact@v6 + with: + name: build-artifacts + path: | + artifacts + buildlog \ No newline at end of file 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..5f32634ff2 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/loopandlearn/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/Gemfile b/Gemfile index 0eb90cb08b..502d6331b5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,2 @@ source "https://rubygems.org" -gem "fastlane", "2.235.0" +gem "fastlane", "2.237.0" diff --git a/Gemfile.lock b/Gemfile.lock index 0089a8cdb4..8e70830ead 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1206.0) - aws-sdk-core (3.241.4) + aws-partitions (1.1267.0) + aws-sdk-core (3.253.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,19 +17,19 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.121.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (1.129.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.211.0) - aws-sdk-core (~> 3, >= 3.241.3) + aws-sdk-s3 (1.226.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) + base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.0.1) + bigdecimal (4.1.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -42,8 +42,9 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.5) + excon (1.5.0) + logger + faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -71,11 +72,11 @@ GEM faraday-retry (1.0.4) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.235.0) + fastimage (2.4.1) + fastlane (2.237.0) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) - addressable (>= 2.8, < 3.0.0) + addressable (>= 2.9.0, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) @@ -87,7 +88,7 @@ GEM csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) + excon (>= 0.71.0, < 2.0.0) faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) @@ -101,9 +102,10 @@ GEM highline (~> 2.0) http-cookie (~> 1.0.5) json (< 3.0.0) - jwt (>= 2.1.0, < 4) + jwt (>= 2.10.3, < 4) logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) multipart-post (>= 2.0.0, < 3.0.0) mutex_m (~> 0.3) naturally (~> 2.2) @@ -124,41 +126,46 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) + google-apis-androidpublisher_v3 (0.104.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.64.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) + google-logging-utils (0.2.0) + googleauth (1.17.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) highline (2.0.3) http-cookie (1.0.8) @@ -166,39 +173,39 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.19.4) - jwt (2.10.2) + json (2.20.0) + jwt (3.2.0) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.19.1) + multi_json (1.21.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) - nkf (0.2.0) + nkf (0.3.0) optparse (0.8.1) os (1.1.4) ostruct (0.6.3) plist (3.7.2) - public_suffix (7.0.2) - rake (13.3.1) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.1.2) + retriable (3.8.0) rexml (3.4.4) rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.21.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally @@ -213,12 +220,14 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) + base64 claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.4.0) + nkf rexml (>= 3.3.6, < 4.0) xcpretty (0.4.1) rouge (~> 3.28.0) @@ -230,7 +239,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane (= 2.235.0) + fastlane (= 2.237.0) BUNDLED WITH 4.0.12 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..48a56f63ce --- /dev/null +++ b/LibreCRKit @@ -0,0 +1 @@ +Subproject commit 48a56f63cee1d4995bd41a89bed7bf919709744d diff --git a/LibreLoop b/LibreLoop new file mode 160000 index 0000000000..b9e817624b --- /dev/null +++ b/LibreLoop @@ -0,0 +1 @@ +Subproject commit b9e817624bad8dbc027421a813e0c9967bcb6b7e diff --git a/LibreTransmitter b/LibreTransmitter index d0d301208f..a38975dd02 160000 --- a/LibreTransmitter +++ b/LibreTransmitter @@ -1 +1 @@ -Subproject commit d0d301208faeb2bc763454baf0550f3fd4888bb7 +Subproject commit a38975dd022b0f424d26e29217143d7b3ff3ca4d 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..f12ef55ca2 160000 --- a/Loop +++ b/Loop @@ -1 +1 @@ -Subproject commit 1f71ec4fa94941abdbd72fd5bd914770faa2e90b +Subproject commit f12ef55ca2aa1571c2ca1c88055828fac1cbd3a4 diff --git a/LoopAlgorithm b/LoopAlgorithm new file mode 160000 index 0000000000..7d016fb55d --- /dev/null +++ b/LoopAlgorithm @@ -0,0 +1 @@ +Subproject commit 7d016fb55ded96ef25ef612a153c69a9c911ca19 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..c9b0c9340c 160000 --- a/LoopKit +++ b/LoopKit @@ -1 +1 @@ -Subproject commit e7e2ee2b546c4d8122014838cb98a0e26dd91208 +Subproject commit c9b0c9340c7bf98ebd44d2fcac8cdc6e30a7004e 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..36b8558f9a 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 -CURRENT_PROJECT_VERSION = 57 +LOOP_MARKETING_VERSION = 3.15.1 +CURRENT_PROJECT_VERSION = 58 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/fastlane/testflight.md b/fastlane/testflight.md index b44f07df07..185aea17e0 100644 --- a/fastlane/testflight.md +++ b/fastlane/testflight.md @@ -12,7 +12,7 @@ These instructions allow you to build your app without having access to a Mac. > The browser build **defaults to** automatically updating and building a new version of Loop according to this schedule: > - automatically checks for updates weekly and if updates are found, it will build a new version of the app > - even when there are no updates, it builds on the second Sunday of the month -> - with each scheduled weekly run, a successful build log appears - if the time is very short, it did not need to build - only the long actions (>20 minutes) built a new app +> - if a scheduled run finds nothing new to build, the run is cancelled and shows as cancelled (grey) in the Actions list - a green check always means a new build was made and uploaded to TestFlight > > The [**Optional**](#optional) section provides instructions to modify the default behavior if desired. 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`