Google Play readiness — Cadencestore_readiness/01_google_play.md · raw .md

Google Play readiness — Cadence

Subject: the app repository at pinned commit 03a176e72ef0075eec86b8915cbe6e93042a3b9d, version 0.4.12+18. Stream: Phase 2.1 (Google Play), plus the Play half of 2.6. All policy retrievals: 2026-08-04 (UTC timestamps in every capture header). Method: every Google page was rendered with utilities/chrome.py (Playwright headless Chromium) per AGENT_RULES R4 — never WebFetch, never requests.get(). Raw captures live in proof/02_store/captures/ with an index at MANIFEST_google_play.md in that folder. Google's help pages hide their requirement tables inside collapsed accordions that the rendered text drops, so every capture also has a <slug>_expanded.txt with the accordions flattened; quote from that file. Repo facts were re-measured, not assumed: proof/02_store/repo_state.txt, repo_state2.txt, repo_state3.txt, merged_manifest.txt (each stamped by proof/run_and_record.sh [not published] with command, cwd, UTC time, Flutter version, git sha and tree state).


0. The one fact that frames everything

https://play.google.com/store/apps/details?id=dev.sergemio.cadence&hl=en&gl=FR returns HTTP 404 (capture gp_store_listing_cadence_probe.txt, retrieved 2026-08-04T10:46:37Z). Cadence has never been published on Google Play. Nothing on the store side exists yet: no account, no listing, no upload key, no declarations. Every "PRESENT" below is a property of the code and build, not of a Play Console that does not exist.


1. Account, identity, payments and the EU trader question

# Requirement Status Evidence Source (retrieved 2026-08-04)
1.1 A Google Play developer account exists MISSING App is 404 on Play; release build type still carries the Flutter template's // TODO: Add your own signing config and points at the debug keystore (android/app/build.gradle.kts:32-34) — no artifact has ever been accepted by Play gp_store_listing_cadence_probe.txt
1.2 US$25 one-time registration fee, paid by credit or debit card (MasterCard, Visa, American Express) MISSING Not paid; no account https://support.google.com/googleplay/android-developer/answer/6112435 — "There is a US$25 one-time registration fee" (gp_registration_fee_expanded.txt:41)
1.3 Account type chosen: personal or organisation MISSING Undecided. This is the single highest-leverage decision on the Play side — see §2 https://support.google.com/googleplay/android-developer/answer/13634885 ; https://support.google.com/googleplay/android-developer/answer/13628312
1.4 Personal account: developer name, legal name, legal address, contact email, contact phone, developer email MISSING answer/13628312, "Creating a developer account for personal use"
1.5 Organisation account: D-U-N-S number, organisation name, address, phone, website, contact name/email/phone, developer email, developer phone MISSING answer/13628312. "This process can take up to 30 days so you should plan ahead. You will not be able to create a developer account for an organization without one."
1.6 Identity verification before publishing (government ID and a credit card in the legal name for personal accounts) MISSING answer/6112435: "you may be asked for a valid government ID and a credit card, both under your legal name. If this information is determined to be invalid, your registration fee will not be refunded."
1.7 Google payments profile linked at account creation, holding the legal name and address MISSING answer/13628312, "Linked Google payments profile": "must be kept up to date to keep your developer account in good standing."
1.8 France is a supported location for developer registration PRESENT (jurisdiction is eligible) France row: developer registration ✔, merchant registration ✔, default currency EUR https://support.google.com/googleplay/android-developer/answer/9306917 (gp_payments_profile_expanded.txt:319-322)
1.9 Merchant account + bank account + tax information MISSING, and only needed if the app monetises Repo has zero billing code (no billing, no IAP, no com.android.vending.BILLING) — see proof/01_findings/S13/network_grep_lib.txt §4 and the merged manifest permission list answer/13628312: "If you decide to monetize on Google Play, you will need to create a merchant account." Payments-profile flow: https://support.google.com/googleplay/android-developer/answer/7161426
1.10 Bank verification (deposit challenge or uploaded bank documents), if monetising MISSING/deferred answer/13628312: "Verification can take up to 5 days, so we recommend starting the process early"
1.11 Store listing contact email (mandatory to publish) MISSING No support email, no website, no mailto: anywhere in the repo — proof/02_store/repo_state.txt §5 prints NO_PRIVACY_OR_SUPPORT_URL https://support.google.com/googleplay/android-developer/answer/13634081: "An email address is required to publish or update apps on Google Play."
1.12 EU trader disclosure (Digital Services Act) MISSING, and it is not a separate form on Play See the finding below answer/13628312

1.12 in full — what a French seller actually has to provide on Google Play

Google Play has no separate trader/non-trader declaration in Play Console Help. Four independent site searches for it returned nothing (gp_search_trader_status, gp_search_trader_only, gp_search_dsa_trader2, gp_search_eea_consumer_law); the only hit is a community-forum thread, which is not an official source. Google's own "General conditions of access for Google Play in the EEA" (https://support.google.com/googleplay/android-developer/answer/14659200) lists the developer's EEA obligations as exactly four: accept the Play Console Terms of Service, pay the one-off US$25 fee, choose an organisational or personal account, verify developer identity information, plus the testing requirement if personal. No trader toggle.

Google satisfies DSA Article 30 traceability automatically, from the payments profile, by publishing the developer's identity rather than by asking for a declaration:

"Google will display your legal name, your country (as per your legal address), and developer email address on Google Play. If you decide to monetize on Google Play then Google will display your full address." — answer/13628312, "Creating a developer account for personal use"

For an organisation: "Google will display your legal name, legal address, developer email address, and developer phone number on Google Play."

Concrete consequence for a French seller. If Serge registers a personal account and ever charges for the app, his home address is published on the Cadence store listing. That is not avoidable by a setting; it is a direct consequence of monetising under a personal account. An organisation account (a French SAS/SARL, or a registered entreprise individuelle if Dun & Bradstreet will issue it a D-U-N-S) publishes the company address instead, and additionally publishes a developer phone number. This is the same trade-off Apple imposes, with one difference worth noting: Apple has an explicit DSA trader declaration screen (captured by the market stream at proof/03_market/captures/policy_apple_asc_dsa_trader.txt), Google does not.

Note the asymmetry with §2: the organisation account that protects the home address is also the account type that is exempt from the 12-tester/14-day gate. The two decisions are the same decision.


2. The new-personal-developer-account testing requirement

The exact current numbers, from the official source.

"If you have a newly created personal developer account, you must run a closed test for your app with a minimum of 12 testers who have been opted-in for at least the last 14 days continuously. When you meet these criteria, you can apply for production access on the Dashboard in Play Console." — https://support.google.com/googleplay/android-developer/answer/14151465, retrieved 2026-08-04T09:53:49Z, capture proof/03_market/captures/policy_google_personal_testing_reqs.txt

# Condition Detail Source
2.1 Who it binds Personal accounts created after November 13, 2023. Organisation accounts are not subject to it. answer/14151465
2.2 Tester count 12 testers minimum, opted in at the moment you apply answer/14151465
2.3 Duration 14 continuous days opted in. Google warns explicitly: "Emphasize to your testers that they need to remain opted in to your closed test for at least 14 days continuously." answer/14151465
2.4 What is locked until you pass Production and Pre-registration are disabled in Play Console answer/14151465
2.5 After the 14 days Apply for production access on the Dashboard; answer three sections of questions (about the closed test, about the app, about production readiness) answer/14151465
2.6 Google's review of that application "This usually takes 7 days or less, but may occasionally take longer." answer/14151465
2.7 Failure mode "If your app isn't ready to be published, you may be required to continue testing your app. Examples include not having 12 testers opted-in to your closed test, or your testers not being engaged with your app during your closed test." answer/14151465
# Requirement Status Evidence
2.0 Closed test with 12 testers opted in for 14 continuous days, then production access granted MISSING No account, no closed test, no testers — app is 404 on Play

What this does to the timeline. On a personal account the shortest possible path to production is: account creation and identity verification → build a signed AAB → complete every App content declaration → publish to closed testing → recruit 12 Google-account holders and keep them opted in → wait 14 days → apply → wait up to 7 days for Google's answer. The 14 days do not start until the closed test is live, and Google grades engagement, not just headcount — a dozen accounts that install and never open the app is the named failure case. On an organisation account, none of this applies and the app can go straight to production once the declarations are complete. The D-U-N-S lead time (up to 30 days) is the price of skipping it, and the two waits do not stack usefully: apply for the D-U-N-S first and the testing gate disappears; skip it and you buy the 14-day gate plus the published home address from §1.12.


3. App signing

# Requirement Status Evidence Source (retrieved 2026-08-04)
3.1 Release artifact is not signed with a debug certificate MISSING — this is a hard blocker android/app/build.gradle.kts:31-35 sets signingConfig = signingConfigs.getByName("debug") under // TODO: Add your own signing config. Both artifacts on disk carry it: the APK (apksigner: CN=Android Debug, O=Android, C=US) and the AAB — openssl pkcs7 on META-INF/ANDROIDD.RSA inside build/app/outputs/bundle/release/app-release.aab prints subject=CN=Android Debug, O=Android, C=US (proof/02_store/repo_state3.txt §B) https://developer.android.com/studio/publish/app-signing — "Because the debug certificate is created by the build tools and is insecure by design, most app stores (including the Google Play Store) do not accept apps signed with a debug certificate for publishing." (and_app_signing_expanded.txt:766-768)
3.2 Upload keystore exists MISSING git ls-files for keystore|.jks|key.properties|.p12|.pepkTRACKED_SIGNING_MATERIAL=none; on-disk check → ON_DISK_SIGNING_MATERIAL=none (proof/02_store/repo_state.txt §2)
3.3 Upload key format: Java keystore (.jks/.keystore), RSA 2048 bits or more MISSING No key https://support.google.com/googleplay/android-developer/answer/9842756 — "Requirement: Must be an RSA key of 2048 bits or more."
3.4 Play App Signing enrolment MISSING (and automatic on first upload) answer/9842756, "For new apps": "Your app will be automatically enrolled in quantum-ready, hybrid signing with Google-generated keys." Google-generated app signing keys are RSA 4096-bit. Accepting the Play App Signing Terms of Service is one of the "Declarations" at app creation (https://support.google.com/googleplay/android-developer/answer/9859152).
3.5 Signing material never committed PRESENT (the guard, not the key) android/.gitignore:12-14 already lists key.properties, **/*.keystore, **/*.jks, under the comment "Remember to never publicly share your keystore." (proof/02_store/repo_state.txt §3) https://docs.flutter.dev/deployment/android — "keep the keystore file private; don't check it into public source control!"
3.6 Gradle wired to the release key MISSING Only the debug config is wired; there is no signingConfigs { create("release") ... } block in android/app/build.gradle.kts https://docs.flutter.dev/deployment/android, "Configure signing in Gradle"

3.a The exact commands

Generate the upload keystore (macOS; verbatim from https://docs.flutter.dev/deployment/android, retrieved 2026-08-04T10:41:31Z, flutter_android_deployment_expanded.txt:789-790):

keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA \
        -storetype JKS -keysize 2048 -validity 10000 -alias upload

-storetype JKS is required on Java 9 and newer, where the default became PKCS12 — the toolchain baseline for this project is openjdk version "17.0.20" 2026-07-21 (proof/00_toolchain/java_version.txt), so keep the flag. If keytool is not on the path, run flutter doctor -v, take the path printed after "Java binary at:", and replace the trailing java with keytool (same source).

Then [project]/android/key.properties (never committed; already gitignored):

storePassword=<password-from-previous-step>
keyPassword=<password-from-previous-step>
keyAlias=upload
storeFile=upload-keystore.jks

Then android/app/build.gradle.kts — verbatim from the same page's Kotlin code blocks. Before the android block:

import java.util.Properties
import java.io.FileInputStream

plugins {
   ...
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
   ...
}

and inside android, replacing the debug line rather than sitting beside it:

    signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Flutter's own documentation shows both signingConfig lines stacked, because it is illustrating a diff. Do not leave both: delete the getByName("debug") line. Two assignments to the same property is exactly the "additive patch" failure mode where the loose default survives and the tightening becomes invisible.

Proof the fix worked — the same command that produced the current failure:

apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apk

must stop printing CN=Android Debug and print the upload key's DN instead. For the bundle, unzip -p build/app/outputs/bundle/release/app-release.aab META-INF/ANDROIDD.RSA | openssl pkcs7 -inform DER -print_certs -noout must do the same.

3.b What happens to today's artifacts

Both are dead on arrival. build/app/outputs/flutter-apk/app-release.apk (53,629,091 bytes, sha256 f11a484d…) and build/app/outputs/bundle/release/app-release.aab (51,974,023 bytes, sha256 89ac5401f8a11bff81b5c29479b0ba036828d30f8b24342bbc0c681ca57f55bb) are both signed CN=Android Debug. Play will not accept either. They are useful only as a demonstration that the build pipeline works end to end; delete them and rebuild once the release signing config lands. There is no salvage path — a debug-signed artifact cannot be re-signed into a valid upload, because the upload key must be the one whose certificate Google registers on first upload.


4. Format and technical requirements

# Requirement Status Evidence Source (retrieved 2026-08-04)
4.1 Android App Bundle (.aab), not APK PRESENT build/app/outputs/bundle/release/app-release.aab exists, 51,974,023 bytes, with base/manifest/AndroidManifest.xml and BUNDLE-METADATA/ debug symbols for all three ABIs (proof/02_store/repo_state3.txt §A) https://support.google.com/googleplay/android-developer/answer/9859152 — "Google Play uses Android App Bundles to generate and deliver APKs"; https://support.google.com/googleplay/android-developer/answer/9859372 — "Starting August 2021, new apps are required to publish with the Android App Bundle on Google Play."
4.2 Target API level PRESENT targetSdkVersion=36 read out of the shipped AAB's protobuf manifest and confirmed in the packaged release manifest: android:targetSdkVersion="36" (proof/02_store/repo_state3.txt §D, proof/02_store/merged_manifest.txt) https://support.google.com/googleplay/android-developer/answer/11926878 — "Starting August 31, 2026: New apps and app updates must target Android 16 (API level 36) or higher to be submitted to Google Play"
4.3 Deadline exposure on target API PRESENT — no exposure Already at 36, the level required from 2026-08-31. Today is 2026-08-04, so the app clears the deadline 27 days before it lands, and no extension request (available to 2026-11-01) is needed. answer/11926878
4.4 minSdk PRESENT android:minSdkVersion="24" in the packaged release manifest
4.5 64-bit native code PRESENT ABIs inside the release APK: arm64-v8a, armeabi-v7a, x86_64 (proof/02_store/repo_state2.txt §D). arm64-v8a satisfies the requirement; armeabi-v7a is the permitted 32-bit companion. https://developer.android.com/google/play/requirements/64-bit
4.6 Base-module download-size limit 500MB PRESENT 51,974,023 bytes ≈ 50 MiB, an order of magnitude under https://support.google.com/googleplay/android-developer/answer/9859372 — base module 500MB; total across all modules and install-time asset packs 4GB; overall ceiling 34 GB
4.7 Under the 200MB mobile-data warning threshold PRESENT Same figure answer/9859372 — "If your app is above 200MB in size, users on a mobile data connection will see a non-blocking dialog when installing"
4.8 Package name is final and correct MISSING — a decision, not a file applicationId = "dev.sergemio.cadence" (android/app/build.gradle.kts:21), still under the Flutter template's // TODO: Specify your own unique Application ID comment (line 20). Confirmed in the packaged manifest: package="dev.sergemio.cadence". https://support.google.com/googleplay/android-developer/answer/9859152 — "Package names for app files are unique and permanent, so please name them carefully. Package names can't be deleted or re-used in the future."

4.8 in full — the irreversible identifier

dev.sergemio.cadence bakes two unresolved things into a permanent string: a personal namespace (sergemio) and a product name that README.md:38 itself calls a placeholder. Once an AAB with this applicationId is accepted by Play, the name is spent forever — not renameable, not reusable, not deletable. If the product ends up called something else, or if the business ends up owned by a company rather than by Serge personally, the store listing will still be served from a package named after neither. Changing it later means a brand-new listing with zero installs, zero reviews and zero ratings.

This decision must be made before the first upload to any track, including internal testing.


5. Permissions and policy — the highest-risk section

The governing document is Google Play's Permissions and APIs that Access Sensitive Information policy. Note the URL: the old ID 13161072 now redirects to https://support.google.com/googleplay/android-developer/answer/16558241 (capture gp_exact_alarm_policy.txt, final URL recorded in its header). Cite the destination.

# Requirement Status Source (retrieved 2026-08-04)
5.1a USE_EXACT_ALARM is policy-eligible for this app, and SCHEDULE_EXACT_ALARM is correctly capped at maxSdkVersion="32" PRESENT answer/16558241
5.1b Exact-alarm declaration filed in Play Console MISSING answer/16558241, Key Considerations
5.2a USE_FULL_SCREEN_INTENT is policy-eligible for auto-grant (core function = setting an alarm) PRESENT answer/16558241 ; answer/13392821
5.2b Full-screen-intent declaration filed (https://goo.gle/play-permission-decl-form) MISSING answer/13392821
5.2c Runtime check and graceful degradation if the permission is not granted MISSING answer/13392821
5.3 POST_NOTIFICATIONS requested at runtime and handled on refusal PRESENT https://developer.android.com/develop/ui/compose/notifications/notification-permission
5.4 RECEIVE_BOOT_COMPLETED — normal permission, no declaration PRESENT answer/16558241 (absent from the restricted list)
5.5 VIBRATE, WAKE_LOCK, MODIFY_AUDIO_SETTINGS — normal permissions, no declaration PRESENT answer/16558241 (absent from the restricted list)
5.6 No foreground service, so no Android 14 FGS declaration and no demo video N/A answer/13392821
5.7 No INTERNET permission PRESENT proof/01_findings/S13/apk_permissions.txt; proof/02_store/merged_manifest.txt

The detail behind each row follows.

5.1 USE_EXACT_ALARMELIGIBLE

The policy names the acceptable use cases exhaustively:

"Your app must use the USE_EXACT_ALARM functionality only when your app's core, user facing functionality requires precisely-timed actions, such as: - The app is an alarm or timer app. - The app is a calendar app that shows event notifications." — answer/16558241, "Acceptable use cases for using the Exact Alarm Permission" (gp_exact_alarm_policy_expanded.txt:625-630)

Cadence is a timer app and nothing else: pubspec.yaml:2 describes it as "Cadence — Kitchen Timer. Pro multi-timer board for restaurant kitchens." The permission drives AlarmManager.setAlarmClock backstop alarms at each running timer's final deadline (lib/alarm_backstop.dart:1-15, :192-194). This is the first bullet, verbatim. The permission is policy-eligible.

Two conditions still attach:

  • The policy summary adds that USE_EXACT_ALARM "is a highly restricted permission… Apps that request this restricted permission are subject to review, and those that do not meet the acceptable use case criteria will be disallowed from publishing on Google Play." Eligible does not mean unreviewed.
  • Key Considerations, "Do": "Complete Play Console declaration to indicate app functionality." Status: MISSING — no declaration can exist without an account.

SCHEDULE_EXACT_ALARM android:maxSdkVersion="32" (android/app/src/main/AndroidManifest.xml, confirmed in the packaged manifest) is the correct API 31-32 fallback and is the permission Google explicitly points at for apps that don't qualify. Cadence carries both, correctly scoped. PRESENT.

5.2 USE_FULL_SCREEN_INTENTELIGIBLE, but the declaration is mandatory and the code does not handle refusal

"For apps targeting Android 14 (API target level 34) and above, USE_FULL_SCREEN_INTENT is a special apps access permission. Apps will only be automatically granted to use the USE_FULL_SCREEN_INTENT permission if the core functionality of their app falls under one of the below categories that require high priority notifications: - setting an alarm - receiving phone or video calls" — answer/16558241, "Full-Screen Intent Permission" (gp_exact_alarm_policy_expanded.txt:662-670)

And the operational rule, with dates:

"If you use the USE_FULL_SCREEN_INTENT permission, you are required to complete the Play Console declaration starting May 31, 2024 to indicate if your app has a permitted core functionality and qualifies for automatic granting. Starting January 22, 2025, for apps targeting Android 14+, only apps that have calling or alarm functionalities will have this permission enabled by default. Otherwise, you must get user permission… For apps that did not complete the declaration or have not been approved for default enabling, developers will need to prompt users to grant permission on new installs and gracefully degrade the experience if denied." — https://support.google.com/googleplay/android-developer/answer/13392821, "Full-screen intent requirements for Android 14" (gp_restricted_permissions_decl_expanded.txt:120-129)

Cadence sets alarms — AndroidNotificationCategory.alarm with fullScreenIntent: true on a channel whose entire purpose is "Rings when a timer expires while the app is not on screen" (lib/alarm_backstop.dart:44-59). The core function is setting an alarm. The permission is policy-eligible for auto-grant.

But there is a real defect behind it. The declaration is required regardless of eligibility, and Google's fallback path — prompt the user, degrade gracefully — is not implemented. lib/alarm_backstop.dart:77-87 requests exactly two permissions:

      final notif = await android?.requestNotificationsPermission();
      final exact = await android?.requestExactAlarmsPermission();

There is no canUseFullScreenIntent() check and no request path; proof/02_store/repo_state.txt §6 shows the complete set of permission calls in lib/ and android/app/src/main/kotlin/ — three lines, none of them full-screen intent. If Google declines the declaration, or if the declaration is simply never filed, the full-screen intent silently downgrades to a heads-up notification on Android 14+. In a kitchen the alarm still makes noise, but it stops seizing the screen — which is the behaviour the whole backstop exists to produce when the tablet is showing something else. The app would neither know nor tell the operator, unlike the exact-alarm path, which does degrade loudly (:209-216 flips _exactOk and raises a critical banner). Status: MISSING (declaration), and a code gap worth a finding in the engineering stream.

5.3 POST_NOTIFICATIONSnot a Play-restricted permission; correctly handled in code

POST_NOTIFICATIONS does not appear anywhere in the Permissions and APIs that Access Sensitive Information policy (grep over gp_exact_alarm_policy_expanded.txt matches only Exact Alarm and Full-Screen Intent, plus the entries for VPN, Age Signals and the rest — no notification entry). It is an ordinary Android 13+ runtime permission requiring a user prompt, not a Play declaration.

Android's own guidance is that the app must request it at runtime and behave sensibly if refused (https://developer.android.com/develop/ui/compose/notifications/notification-permission). Cadence does: alarm_backstop.dart:79 calls requestNotificationsPermission(), and :83-87 raises a critical operator banner when it comes back false — "permission notifications REFUSEE" — with the comment "without it the safety net can ring but shows nothing — say it loudly". PRESENT. No store action required.

5.4 RECEIVE_BOOT_COMPLETEDnot restricted, no declaration

Also absent from the sensitive-permissions policy. It is a normal-protection permission granted at install. It is merged in for flutter_local_notifications' ScheduledNotificationBootReceiver, which re-registers pending backstop alarms after a reboot (android/app/src/main/AndroidManifest.xml, receiver block). PRESENT. No store action required.

5.5 The rest of the manifest

VIBRATE, WAKE_LOCK, MODIFY_AUDIO_SETTINGS are normal permissions and appear in no Play restricted-permission policy. PRESENT.

No foreground service. The packaged release manifest declares no FOREGROUND_SERVICE* permission and no android:foregroundServiceType (proof/02_store/merged_manifest.txt — the full uses-permission list is VIBRATE, WAKE_LOCK, MODIFY_AUDIO_SETTINGS, POST_NOTIFICATIONS, USE_EXACT_ALARM, SCHEDULE_EXACT_ALARM≤32, USE_FULL_SCREEN_INTENT, RECEIVE_BOOT_COMPLETED, plus the plugin-generated dev.sergemio.cadence.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION). The Android 14 foreground-service declaration on the App content page therefore does not apply. N/A — and this is a good outcome: the FGS declaration would have required a demo video per service type (answer/13392821).

No INTERNET permission. INTERNET_PRESENT = False in the shipped APK's binary manifest (proof/01_findings/S13/apk_permissions.txt), and it is absent from the packaged release manifest. This single fact carries most of §6.


6. Data safety form

Every app must complete it, including apps that collect nothing.

"Even developers with apps that do not collect any user data must complete this form and provide a link to their privacy policy. In this case, the completed form and privacy policy can indicate that no user data is collected or shared." — https://support.google.com/googleplay/android-developer/answer/10787469

Apps active only on the internal testing track are exempt; closed, open and production tracks all require it (same source). Since a personal account must run a closed test (§2), the form is required before the 14-day clock can even start.

Reconciliation note (asked for explicitly). findings/S13_data.md does not exist as of 2026-08-04 — findings/ contains S1_engine.md, S2_persistence.md, S5_error_handling.md, S10_product.md, S11_LICENSES_draft.md and nothing else. S13's raw probes do exist at proof/01_findings/S13/ (probe_data.txt, network_grep_lib.txt, apk_permissions.txt, device_info_reads.txt, transitive_http_reachability.txt, no_config_export_import.txt), and the answers below are derived from those probes plus the manifest. Every row must be re-checked against findings/S13_data.md when S13 lands.

# Form question Required answer Evidence
6.1 Does your app collect or share any of the required user data types? No "Collect" is defined as "transmitting data from your app off a user's device". The app has no INTERNET permission (proof/01_findings/S13/apk_permissions.txt) and no network symbol anywhere in lib/ — the grep for package:http|HttpClient|WebSocket|Socket(|package:dio|Uri.parse|… exits 1, no match (network_grep_lib.txt §1). Transmission is not merely absent, it is impossible.
6.2 Is any data collected by third-party libraries or SDKs? No No analytics, crash-reporting or ad SDK: the grep for firebase\|crashlytics\|sentry\|analytics\|amplitude\|mixpanel\|posthog\|bugsnag\|appcenter\|datadog\|google_mobile_ads\|facebook across pubspec.yaml and pubspec.lock exits 1 (network_grep_lib.txt §4)
6.3 Is data shared with third parties? No Nothing leaves the device automatically. The journal export (lib/journal.dart:224-229) writes a text file to a temp/external directory and hands it to the system share sheet — an on-device transfer to another app, which the policy would normally require disclosing, except under the named exception: "User-initiated action… Transferring user data to a third party based on a specific user-initiated action, where the user reasonably expects the data to be shared". The operator taps Export and picks the destination. Declare No, and state the export in the privacy policy so the two documents agree.
6.4 Data types collected (Location / Personal info / Financial / Health / Messages / Photos & videos / Audio / Files & docs / Calendar / Contacts / App activity / Web browsing / App info & performance / Device or other IDs) None selected Everything the app stores is local: 11 SharedPreferences keys and one file, <appdir>/cadence-journal.txt (proof/01_findings/S13/probe_data.txt). Note the content is sensitive in substance even though it is never collected — a real probe row reads "phrase":"Table 12 pour Mme Dupont", a customer's name typed by a cook. It stays on the tablet.
6.5 Purposes (App functionality / Analytics / Developer communications / Advertising / Fraud prevention / Personalization / Account management) N/A — no data types selected
6.6 Required vs optional per data type N/A
6.7 Is data encrypted in transit? N/A — nothing is in transit Same evidence as 6.1
6.8 Can users request data deletion? N/A, but answer honestly if the console forces a value: the app has no accounts and no server; the user deletes data by clearing app storage or uninstalling No account system anywhere in lib/; Play's account-deletion requirement (https://support.google.com/googleplay/android-developer/answer/13327111) binds apps that let users create an account — Cadence does not
6.9 Privacy policy URL (blocking prerequisite for the form) see §9.1 — counted there, not twice proof/02_store/repo_state.txt §5 → NO_PRIVACY_OR_SUPPORT_URL
6.10 Optional badges (Families commitment, Independent Security Review/MASA, UPI) skip all three answer/10787469

The rows above are the answers. The one counted checklist item is the form itself:

# Requirement Status Source
6.0 Data safety form completed and submitted MISSING https://support.google.com/googleplay/android-developer/answer/10787469

7. Content rating, target audience, and the other App content declarations

Everything here lives on the App content page (Play Console → Policy and programs → App content) and is described at https://support.google.com/googleplay/android-developer/answer/9859455.

# Declaration Status Required answer for Cadence Source
7.1 Content rating questionnaire (IARC) MISSING Complete it. Category will be an app (not a game); a kitchen timer has no rateable content, which yields the lowest rating from every authority. https://support.google.com/googleplay/android-developer/answer/9859655 — "To prevent your apps from being listed as 'Unrated'… 'Unrated' apps may be removed from Google Play." The questionnaire also requires an email address for IARC correspondence.
7.2 Content rating retake after content changes N/A (applies to later updates only) Required for "All app updates where there has been a change to your content or features that would affect the responses" answer/9859655
7.3 Target audience and content MISSING Declare adult age groups only — "not designed for children". This is the answer that keeps the app out of Google Play's Families policy requirements entirely. A professional kitchen tool has no plausible child audience. answer/9859655, "Understanding your target audience selection"; https://support.google.com/googleplay/android-developer/answer/9867159
7.4 Ads declaration MISSING No. The app contains no ads of any kind: no ad SDK in pubspec.yaml/pubspec.lock (network_grep_lib.txt §4), no cross-promotion, no house ads. answer/9859455, "Ads": "You must declare whether or not your app contains ads… If you misrepresent the presence of ads… may result in your app(s) being suspended."
7.5 News and Magazine apps MISSING No. answer/9859455, "News and Magazine apps"; https://support.google.com/googleplay/android-developer/answer/10523915
7.6 COVID-19 contact tracing and status apps MISSING No — select none of the statements. answer/9859455, "COVID-19 contact tracing and status apps"
7.7 Health apps declaration MISSING No health features. A kitchen timer is not a health app; it offers no fitness, medical, mental-health or health-research feature. https://support.google.com/googleplay/android-developer/answer/14738291; category list at https://support.google.com/googleplay/android-developer/answer/13996367
7.8 Government apps N/A Not a government app https://support.google.com/googleplay/android-developer/answer/9514050
7.9 Financial features declaration N/A No financial features, no billing code Play Financial Services policy
7.10 Sign-in details for reviewers N/A The app has no login, no gate, no account — the reviewer and the pre-launch crawler reach the full board on first launch answer/9859455, "Sign-in details" applies only where access is "restricted based on login credentials, sign in details, memberships, location, or other forms of authentication"
7.11 Permissions declaration form MISSING Required for the exact-alarm and full-screen-intent declarations in §5. Not required for SMS/Call Log — the app requests neither. answer/9859455, "Permissions declaration form"; the FSI form is linked from the policy as https://goo.gle/play-permission-decl-form
7.12 Data safety counted at §6.0 §6 answer/10787469
7.13 Privacy policy URL counted at §9.1 §9 answer/9859455
7.14 Developer Program Policies + US export laws acknowledgements, Play App Signing ToS MISSING Accepted in the "Declarations" step at app creation https://support.google.com/googleplay/android-developer/answer/9859152

8. Store listing assets

Requirements below are quoted from https://support.google.com/googleplay/android-developer/answer/9866151, retrieved 2026-08-04T10:34:01Z. The pixel figures are inside collapsed accordions; they are in gp_graphic_assets_expanded.txt, not in the plain rendered text.

# Asset Exact requirement Status Evidence
8.1 App name / title 30 characters or less MISSING Product name unresolved — README.md:38 calls "Cadence" a placeholder. android:label="Cadence" today.
8.2 App icon 32-bit PNG (with alpha), 512px × 512px, maximum file size 1024KB MISSING Nothing near 512×512 exists. Largest launcher icon is mipmap-xxxhdpi/ic_launcher.png at 192×192; largest adaptive foreground is drawable-xxxhdpi/ic_launcher_foreground.png at 432×432 (proof/02_store/repo_state2.txt §C). The store icon is a separate, higher-resolution asset — "The app icon does not replace your app's launcher icon but should be a higher-fidelity, higher-resolution version".
8.3 Short description 80 character limit MISSING No store copy exists in the repo (NO_STORE_ASSET_FOLDER, proof/02_store/repo_state.txt §9)
8.4 Full description 4,000 characters MISSING Same
8.5 Feature graphic JPEG or 24-bit PNG (no alpha), 1024px × 500px MISSING Same. Required to publish the listing. Note the guidance to keep the focal point centred and to avoid pure white, black or dark grey, which blend into Play's background.
8.6 Screenshots — the floor "You must provide a minimum of two screenshots across different device types to publish your store listing: JPEG or 24-bit PNG (no alpha); minimum dimension 320px; maximum dimension 3840px; the maximum dimension can't be more than twice the minimum dimension." MISSING No screenshots anywhere in the repo
8.7 Screenshots — large screens (tablets, Chromebooks) "you can add a minimum of 4 screenshots to demonstrate your in-app experience. Upload screenshots between 1,080 and 7,680px. Use a 16:9 aspect ratio for landscape and a 9:16 aspect ratio for portrait." Up to 8 per device type. MISSING — and this is the set that actually matters for Cadence The product is a tablet board for a kitchen, not a phone app: the audit's own product stream reasons throughout in tablets, including the repo's "petite tablette" reference resolution of 800 × 540 (findings/S10_product.md:459) and "116-296 taps… per tablet" (:80)
8.8 Screenshots — promotional eligibility "For apps, you must provide at least four screenshots with minimum 1080px resolution. These should be 16:9 for landscape (minimum 1920×1080px) and 9:16 for portrait (minimum 1080×1920px)." MISSING Highly recommended, not mandatory — but it is the gate for appearing in Play's large-format recommendation surfaces
8.9 Preview video Optional. One YouTube URL, public or unlisted, monetisation off, not age-restricted, embeddable, no playlist/channel URLs, no timecode parameters. MISSING (optional)
8.10 TV banner 1280px × 720px N/A Not an Android TV app — no leanback intent filter in the manifest
8.11 Wear OS / Automotive / XR screenshots Device-specific N/A Not distributed to those form factors
8.12 Category and tags Choose one MISSING Not chosen. "Tools" or "Food & Drink" are the plausible candidates; this is a marketing call, not a compliance one.
8.13 Alt text on every graphic asset 140 characters or less, no "photo of"/"image of" MISSING Recommended, and it is free accessibility

8.a What tablet-first changes

Play's hard floor is only "two screenshots across different device types" — no specific device type is individually mandatory. But a kitchen board photographed on a phone would misrepresent the product to the exact buyer being targeted, and Cadence's dense multi-tile board is the thing worth showing. The right set is at least four landscape 16:9 large-screen screenshots at 1920×1080 or better (satisfying 8.7 and 8.8 at once), taken on a tablet-shaped viewport, plus a small phone set only if the app is genuinely to be sold for phone use. Everything must be captured from the real app: "Screenshots must demonstrate the actual in-app or in-game experience"; no device frames, no marketing overlay taking more than 20% of the image, and — a rule the audit's own French UI makes easy to trip — the notification bar must be cleaned of service-provider names and notifications before capture.


9. Privacy policy URL

# Requirement Status Source
9.1 An active, public URL hosting a privacy policy, entered on the App content page MISSING https://support.google.com/googleplay/android-developer/answer/9859455
9.2 Required even with zero data collection, because the Data safety form cannot be submitted without it MISSING https://support.google.com/googleplay/android-developer/answer/10787469
9.3 Linked within the app as well as on the store listing, for apps requesting sensitive permissions MISSING answer/9859455: "For apps that request access to sensitive permissions or data (as defined in the User data policy): You must link to a privacy policy on your app's store listing page and within your app." Cadence declares two permissions that Google files under Permissions and APIs that Access Sensitive Information (USE_EXACT_ALARM, USE_FULL_SCREEN_INTENT — answer/16558241), so ship the in-app link. It costs one row in a settings sheet and removes the argument.
9.4 Content must "comprehensively disclose how your app collects, uses, and shares user data… including the types of parties with whom it's shared" MISSING answer/9859455, "Privacy policy"

What the policy must actually contain, given this app's data. Another agent is drafting it; this is the specification it has to meet, derived from the probes rather than from a template:

  1. Statement of no collection and no transmission, and the reason: the app declares no INTERNET permission, contains no networking code, and includes no analytics, crash-reporting or advertising SDK.
  2. What is stored locally, named exactly: the 11 SharedPreferences keys observed in a real session — timer definitions (cadence-timers-v1), run state (cadence-run-v1), zone/clone/sound/language/volume settings — and the on-disk journal cadence-journal.txt (proof/01_findings/S13/probe_data.txt).
  3. The free-text warning: timer names and spoken announcement phrases are typed by staff and can contain third-party personal data. The probe's own seed row is "phrase":"Table 12 pour Mme Dupont". The policy must say that this text is stored in clear on the device, is included in any exported journal, and that the operator controls what is typed.
  4. The export path: how the journal leaves the device (user-initiated share sheet only, to a destination the operator picks), and that once shared it is outside the app's control.
  5. Permissions and why each is held: exact alarms and full-screen intent for the alarm backstop, notifications to display it, wake lock to keep the board alive, vibration, and audio-settings modification to drive the alarm stream.
  6. Retention and deletion: data persists until app storage is cleared or the app is uninstalled; there is no server-side copy and therefore no deletion request to make.
  7. Controller identity and contact, matching the Play developer identity in §1.12 — the same legal name and the same contact email that is entered as the store-listing contact.
  8. French and English, since the app ships a French UI and targets French kitchens (cadence-lang: fr in the probe), and the store listing will be French.

10. Release tracks and the first release

# Item Detail Source
10.1 Internal testing Up to 100 testers, available within minutes of upload, can start before the app is fully set up. Apps active only on this track are exempt from the Data safety form. First-time publishes show "a temporary name and store listing information for up to 48 hours". https://support.google.com/googleplay/android-developer/answer/9845334 ; answer/10787469
10.2 Closed testing Controlled tester list; the track that satisfies the §2 gate. Testers need a Google Account or Google Workspace account. Test-user feedback does not affect the public rating. answer/9845334
10.3 Open testing Publicly discoverable and joinable; "Open testing is available when you have production access" — so it is behind the personal-account gate, not a way around it. https://support.google.com/googleplay/android-developer/answer/14151465
10.4 Production Disabled on a new personal account until the closed-test application is approved answer/14151465
10.5 Staged rollout Not available for a first release: "Staged rollouts can only be used for app updates, not when publishing an app for the first time." https://support.google.com/googleplay/android-developer/answer/6346149
10.6 Pre-launch report Automatic, "subject to capacity within our device lab", generated on app-bundle upload and when a production release is saved. Google installs the app on real lab devices and crawls it for several minutes, testing stability, Android compatibility, performance and accessibility. https://support.google.com/googleplay/android-developer/answer/9842757
10.7 Review time "For certain developer accounts, we'll take more time to thoroughly review your app… review times of up to seven days or longer in exceptional cases." Changes are not sent for review automatically — they queue under "Changes not yet sent for review" until submitted. https://support.google.com/googleplay/android-developer/answer/9859751

10.a What the first release should actually use

Upload to internal testing first, immediately after the release signing config lands and before any store copy is written. Three reasons, each backed above: it is the only track exempt from the Data safety form, so it unblocks while §6 and §9 are still being written; it triggers the pre-launch report, which crawls the app on real Google devices — and for an app whose entire coverage baseline shows main.dart, ui/home.dart, ui/tile.dart, ui/header.dart, ui/theme.dart and ui/logo.dart at 0.00% line coverage, a free automated crawl on real hardware is the cheapest UI evidence available anywhere in this project; and it costs nothing irreversible except the package name, which must be settled first anyway.

Then closed testing with the 12 real testers — and they should be real cooks on real tablets, not friends, because Google grades engagement and because the pre-launch crawler cannot tell you whether a timer board survives a dinner service.

Do not plan a staged rollout for launch. It does not exist for a first publish.


Score

Counted rows are the numbered checklist rows carrying a bolded status. The §6 answer table (6.1-6.10) is not counted — those are the answers the form needs, and the form itself is the single counted row 6.0. Rows 7.12 and 7.13 point at 6.0 and 9.1 rather than double-counting them.

Count
PRESENT 15
MISSING 45
N/A (justified, no action) 7

PRESENT (15): 1.8, 3.5, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 5.1a, 5.2a, 5.3, 5.4, 5.5, 5.7.

MISSING (45): 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.9, 1.10, 1.11, 1.12, 2.0, 3.1, 3.2, 3.3, 3.4, 3.6, 4.8, 5.1b, 5.2b, 5.2c, 6.0, 7.1, 7.3, 7.4, 7.5, 7.6, 7.7, 7.11, 7.14, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 8.12, 8.13, 9.1, 9.2, 9.3, 9.4.

N/A (7): 5.6, 7.2, 7.8, 7.9, 7.10, 8.10, 8.11.

The shape of that ratio is worth naming: the engineering is nearly compliant and the paperwork does not exist. Target API, 64-bit, bundle format, size, permission eligibility and runtime permission handling are all already correct — several of them by deliberate design rather than luck. Everything missing is either an account that has not been opened, a document that has not been written, or an image that has not been drawn. The one exception, and the one true engineering blocker, is signing.


Ordered action list

Owner key: S = Serge (repo, build, signing key), H = the project owner (business identity, money, legal), E = either. ⚠ marks an action that is irreversible or effectively irreversible once taken.

Round 0 — decisions that must be made before anything is uploaded

# Action Owner Blocks Irreversible
1 Decide the account type: personal or organisation. Organisation means a D-U-N-S number (free, up to 30 days to obtain), the company address published instead of a home address, and exemption from the 12-tester/14-day gate. Personal means US$25 and a 2-3 week testing gate, plus Serge's home address published the moment the app is monetised. If there is any intention to charge restaurants, this is not a close call. H 2, 3, 12 ⚠ D-U-N-S must be obtained before the organisation account is created; Google will not create one without it
2 Settle the product name and the package name together. dev.sergemio.cadence is permanent once uploaded to any track and encodes both a personal namespace and a name README.md:38 calls a placeholder. Decide the ≤30-character store title at the same time — that half is changeable, the package name is not. H (name), S (edit) 8, 9, 19 Yes — the package name can never be changed, deleted or reused
3 If organisation: apply for the D-U-N-S number now. Up to 30 days. Nothing else in this list is blocked by it, so start it and continue in parallel. H 12 No

Round 1 — unblock the build (fully parallel with Round 0)

# Action Owner Blocks Irreversible
4 Generate the upload keystore with the keytool command in §3.a. RSA, 2048 bits minimum, -validity 10000. S 5, 6, 7 ⚠ Practically — the certificate registered on first upload is the one Play will expect forever, though Google can reset a lost upload key on request
5 Create android/key.properties per §3.a. Already gitignored (android/.gitignore:12); verify with git status --porcelain that it does not appear. S 6 No
6 Wire signingConfigs.create("release") into android/app/build.gradle.kts and DELETE the signingConfig = signingConfigs.getByName("debug") line. Not comment it out, not leave it above the new one — delete it. S 7 No
7 Rebuild and prove it. flutter build appbundle --release, then apksigner verify --print-certs on the APK and openssl pkcs7 on the AAB's META-INF/*.RSA must both stop printing CN=Android Debug. Delete the two debug-signed artifacts currently on disk. S 19 No
8 Store the keystore and its passwords somewhere that survives a lost laptop, off the repo. Losing the upload key is recoverable through Google; losing it with no backup and no Play account is not. S No

Round 2 — content that takes real time to produce (parallel with Round 1)

# Action Owner Blocks Irreversible
9 Write and host the privacy policy to the eight-point specification in §9, in French and English, at a stable public URL. Another agent is drafting the text; someone must own the hosting. H 14, 15, 21 No
10 Add an in-app privacy-policy link (§9.3). One row in the settings sheet. S 21 No
11 Produce the store assets (§8): 512×512 32-bit PNG icon with alpha ≤1024KB; 1024×500 feature graphic, JPEG or 24-bit PNG with no alpha; at least four landscape 16:9 large-screen screenshots at 1920×1080 or better, captured from the real app on a tablet-shaped viewport with a clean status bar; short description ≤80 characters; full description ≤4000; alt text for every image. E 20 No

Round 3 — open the account

# Action Owner Blocks Irreversible
12 Create the Play Console developer account, pay the US$25 fee, link the Google payments profile, and pass identity verification (government ID + card in the legal name for personal; D-U-N-S + organisation documents for organisation). H everything below ⚠ The account type is chosen here
13 Create the app record: default language, app-or-game, free-or-paid, store-listing contact email, and accept the Developer Program Policies, US export laws and Play App Signing Terms of Service declarations. Play App Signing enrolment happens automatically on first upload with Google-generated keys. H 19 ⚠ Yes — the package name is claimed on first upload, and the Google-held app signing key is not something you swap later

Round 4 — the declarations (all parallel; none can start before 12)

# Action Owner Blocks Irreversible
14 Data safety form — answers in §6. Requires the privacy policy URL from action 9. H 22 No
15 Privacy policy URL entered on the App content page. H 14, 22 No
16 Content rating questionnaire (IARC) — an app, not a game; no rateable content. H 22 No
17 Target audience and content — adult only, "not designed for children". H 22 No
18 The one-word declarations: Ads = No. News and Magazine = No. COVID-19 contact tracing/status = none. Health apps = no health features. Government apps, Financial features, Sign-in details = not applicable. H 22 No
19 The two permission declarations, and they are the ones that can get the app rejected: (a) the exact-alarm declaration, stating that the app's core user-facing functionality is a timer app — this is the first bullet of Google's own acceptable-use list, quoted verbatim in §5.1; (b) the full-screen-intent declaration at https://goo.gle/play-permission-decl-form, stating that the core function is setting an alarm. Both are eligible. Neither is automatic. H, with wording from S 22 No
20 Complete the main store listing: title, short and full descriptions, icon, feature graphic, screenshots, category and tags. E 22 No

Round 5 — release

# Action Owner Blocks Irreversible
21 Add canUseFullScreenIntent() handling in lib/alarm_backstop.dart: check the grant, and if it is absent, raise the same critical operator banner the exact-alarm path already raises at :211-216. Google requires apps that are not auto-granted to "gracefully degrade the experience if denied", and today the app neither checks nor tells anyone. This does not block submission; it blocks the app being honest with a kitchen. S No
22 Upload the signed AAB to internal testing. Exempt from the Data safety form, triggers the free pre-launch report on real Google lab devices, available to testers within minutes. Fix whatever the crawl finds. S 23 ⚠ First upload claims the package name permanently
23 If personal account: publish to closed testing, recruit 12 real testers on real tablets, keep them opted in and engaged for 14 continuous days, then apply for production access and allow up to 7 more days for Google's answer. If organisation account: skip straight to 24. H 24 No
24 Publish to production. No staged rollout — it does not exist for a first release. H No

The critical path

Organisation account: 3 → 12 → 13 → (14…20 in parallel with 4…11) → 22 → 24. The long pole is the D-U-N-S, up to 30 days, and it can be started today.

Personal account: 12 → 13 → (14…20) → 22 → 23 → 24. The long pole is 14 days of closed testing plus up to 7 days of review, and it cannot start until every declaration in Round 4 is finished, because a closed-testing track still requires the Data safety form.

02 — Apple App Store readiness for Cadencestore_readiness/02_apple_app_store.md · raw .md

02 — Apple App Store readiness for Cadence

Stream: 02 (Phases 2.2, 2.3, 2.4) Subject: the app repository at pinned sha 03a176e72ef0075eec86b8915cbe6e93042a3b9d, version 0.4.12+18 Written: 2026-08-04 Companion: store_readiness/04_ios_blockers.md — the technical viability assessment and the Android/iOS alarm-parity gap. Captures: proof/02_store/captures/, indexed by MANIFEST_apple.md — 70 rows covering 66 distinct Apple URLs fetched today, each with url, final url, HTTP status, page title, byte count, attempt count and UTC retrieval time (four URLs appear twice because a first attempt was retried or replaced). Earlier-stream Apple captures reused here live in proof/03_market/captures/ and are named individually where cited.


0. Method and its limit

Every Apple policy fact below was fetched today with utilities/chrome.py (Playwright headless Chromium shell) and stored. developer.apple.com documentation is a single-page application; networkidle never settles on it under load, so captures used wait_until=domcontentloaded with a 6-second settle and up to three attempts. Nothing is quoted from memory.

Xcode is not installed on this machine (Command Line Tools only) and CocoaPods is absent. No iOS build of Cadence has ever existed. The app's own iOS adapter says so at ios/Runner/AppDelegate.swift:12-13: ⚠️ NOT YET COMPILED — written on Windows, no Xcode available. Every claim below about the native iOS layer — the Xcode project, Info.plist, the asset catalogue, signing — is therefore static analysis of files, and is labelled as such.

One exception, and it is the important one: the Dart half of the app can be executed here, under the Flutter test binding with a platform override. The alarm finding (C18) was run, not merely read — see proof/02_store/probe_ios_backstop.txt and 04_ios_blockers.md §2.7. That execution happened on a cp -R copy at a scratch working copy; the pinned repo was not modified (git status --porcelain empty, HEAD still 03a176e), per R10.

Where a question can only be settled on a device or inside Apple's systems, the exact artifact that would settle it is named in §11.


1. Scoreboard

16 PRESENT · 51 MISSING · 1 PARTIAL — across 68 checklist items.

Bucket Items PRESENT PARTIAL MISSING
A. Seller entity and Apple Developer Program 8 0 0 8
B. Identifiers, certificates, provisioning 8 1 0 7
C. Binary and bundle compliance 18 10 1 7
D. App Store Connect record and metadata 21 0 0 21
E. Store listing assets 6 2 0 4
F. App Review Guidelines exposure 7 3 0 4
Total 68 16 1 51

The 11 hard blockers, named

These are the MISSING items that make a submission impossible or make rejection certain. Every other MISSING item is work, not a wall.

Item Why it is a wall
A1 — no Apple Developer Program membership Nothing else in bucket B, D or E can even be started.
A7 — DSA trader status undeclared Apple's required-properties table marks it Required, and Apple states it is required even for developers who do not distribute in the EU.
B7 — no App Store Connect app record There is nothing to submit to.
B8 — no iOS build has ever existed No Xcode, no CocoaPods, no signing identity, and a target that has never compiled.
C1 — no PrivacyInfo.xcprivacy The app's own executable reaches two required-reason API categories (§4.1). "Starting May 1, 2024, apps that don't describe their use of required reason API in their privacy manifest file aren't accepted by App Store Connect."
C4 — the iOS icon is the stock Flutter logo Guideline 2.1: "We will reject incomplete app bundles." A template icon is the definition of incomplete.
C18 — the app cannot ring when backgrounded or locked on iOS BLOCKER under AGENT_RULES.md R13 ("fails to ring an alarm"), and it ships a permanent ⚠️ Backup alarm unavailable banner straight into the reviewer's hands.
D11 — no age rating "An Unrated app can't be published on the App Store."
D13 — no support URL Marked Required by Apple; Guideline 2.1(a) additionally bans "empty websites".
D15 — no privacy policy URL, and no in-app privacy link "This is required for all apps", plus Guideline 5.1.1(i)'s in-app requirement.
D17 — App Privacy answers not published Required before the product page can go live.

One decision is irreversible once used: the bundle identifier (I2 in §10). A wrong choice there cannot be undone without a new app record, a new store listing, and no upgrade path for already-installed users.


2. The checklist

Legend — P = present and correct in the repository or the account; M = missing; PT = partially present. "Owner" is the project owner (H) where the item is an account, a legal identity, a payment, a URL to be hosted, or a commercial decision; Serge (S) where it is code, a project setting, or an asset in the repository.

A. Seller entity and Apple Developer Program

# Item State Evidence Owner
A1 Apple Developer Program membership, active and paid M No account exists for this project. Fee: "The Apple Developer Program annual fee is 99 USD … in local currency where available." — https://developer.apple.com/support/enrollment/ (final url /help/account/membership/program-enrollment/), capture adp_enrollment.txt line 105, HTTP 200, retrieved 2026-08-04T10:48:04Z H
A2 Enrolment type decided: individual vs organisation M Individual: "If you're an individual or sole proprietor/single-person business, your personal legal name will be listed as the seller on the App Store. Do not enter an alias, nickname, or company name as your first or last name." Organisation: "the legal entity name will be listed as the seller … We'll ask for your legal entity name and your D‑U‑N‑S® Number." — same capture, lines 69 and 73. This decision is effectively irreversible in practice (A6). H
A3 D-U-N-S number (organisation route only) M "Your organization must have a D‑U‑N‑S Number … These unique nine-digit numbers are assigned by Dun & Bradstreet … They're free in most jurisdictions."adp_enrollment.txt:80. Lead time: "please allow up to 5 business days to receive your number from D&B" then "up to 2 business days for Apple to receive your information from D&B." — https://developer.apple.com/support/D-U-N-S/, capture adp_duns.txt:82,84, HTTP 200, retrieved 2026-08-04 H
A4 Legal entity exists (organisation route only) M "To enroll in the Apple Developer Program, your organization must be a legal entity so that it can enter into contracts with Apple. We don't accept DBAs, fictitious businesses, trade names, or branches."adp_enrollment.txt:78 H
A5 Binding-authority reference (organisation route, non-founder only) M "if you are not the owner/founder, you must provide a reference who can confirm that you are an employee with the legal authority to enroll the organization."adp_enrollment.txt:93 H
A6 Awareness that individual → organisation conversion is a support ticket, not a setting M "If you have enrolled as an individual and need to convert your individual account to an organization account, please contact us."adp_enrollment.txt:146. Treat as one-way for planning. H
A7 EU Digital Services Act trader status declared M It is a required App-information property: the required-properties table lists Digital Services Act (DSA) Status under Required. — https://developer.apple.com/help/app-store-connect/reference/app-information/required-localizable-and-editable-properties, capture asc_ref_required_properties.{txt,html}, HTTP 200, retrieved 2026-08-04. Apple: "Even if you don't distribute apps in the EU, you'll still need to declare a trader status." — capture proof/03_market/captures/policy_apple_asc_dsa_trader.txt, analysed at research/_raw_03_store_policy.md §D H
A8 DSA trader contact set that Apple will publish (address / P.O. Box, phone, email) M "Articles 30 and 31 of the Digital Services Act (DSA) require Apple to verify and display trader contact information … Once verified, Apple will publish this information on your App Store product page when your app is distributed in any of the 27 territories of the EU." Individuals must enter "Address or P.O. Box; Phone number; Email address." — same capture, quoted at research/_raw_03_store_policy.md §D.3 H

A-bucket consequence, stated plainly. If Serge enrols as an individual, his legal name is the public seller on every Cadence App Store page, worldwide, permanently, and in the EU his address (or a documented P.O. Box), phone number and email are published on the product page as well. If the product is to be sold to restaurants under a business identity, the organisation route is the only one that produces a company seller name — and it needs a legal entity plus a D-U-N-S number first, which adds up to seven business days of D&B and Apple lead time before enrolment can even begin.

B. Identifiers, certificates, provisioning

# Item State Evidence Owner
B1 Bundle identifier chosen in the Xcode project P PRODUCT_BUNDLE_IDENTIFIER = dev.sergemio.cadenceios/Runner.xcodeproj/project.pbxproj:385 (and dev.sergemio.cadence.RunnerTests at :401) S
B2 App ID registered in Certificates, Identifiers & Profiles M Requires an Apple Developer account (A1). Procedure: https://developer.apple.com/help/account/manage-identifiers/register-an-app-id, capture register_app_id.txt, HTTP 200, retrieved 2026-08-04. IRREVERSIBLE — see §10. H
B3 DEVELOPMENT_TEAM set in the Xcode project M grep -n "DEVELOPMENT_TEAM" ios/Runner.xcodeproj/project.pbxproj → no match S
B4 Apple Distribution certificate M The project still carries the legacy "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer" (project.pbxproj:349), which is a development identity, not a distribution one H creates, S wires
B5 App Store provisioning profile M grep -n "PROVISIONING_PROFILE" ios/Runner.xcodeproj/project.pbxproj → no match. Automatic signing is enabled only on the RunnerTests target (CODE_SIGN_STYLE = Automatic, :397), not on Runner H
B6 Entitlements file M find ios -name "*.entitlements" → empty; grep CODE_SIGN_ENTITLEMENTS project.pbxproj → no match. Blocks Time Sensitive notifications and Critical Alerts (04_ios_blockers.md §5-6) S
B7 App Store Connect app record created M Procedure: https://developer.apple.com/help/app-store-connect/create-an-app-record/add-a-new-app, capture asc_add_new_app.txt, HTTP 200, retrieved 2026-08-04 H
B8 A build has ever been uploaded (TestFlight or App Store) M Impossible today — no Xcode, no CocoaPods, no signing identity, and the iOS target has never compiled. Upload procedure captured at upload_build.txt; TestFlight at testflight_overview.txt H + S

C. Binary and bundle compliance

# Item State Evidence Owner
C1 PrivacyInfo.xcprivacy at the app bundle root M find . -name "*.xcprivacy" -not -path "./build/*"no results. Required location: "If you build an iOS, iPadOS, tvOS, visionOS, or watchOS app, place the privacy manifest at the root of your app's bundle" — https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk, capture privacy_adding_manifest.txt. Exact XML in §4. S
C2 Third-party SDK privacy manifests present for the listed SDKs PT Cadence links nine iOS plugins (.flutter-plugins-dependencies). Verified in ~/.pub-cache by find -name "*.xcprivacy". Ship a manifestflutter_local_notifications 22.1.0, device_info_plus 13.2.0, share_plus 13.3.0, shared_preferences_foundation 2.5.6, wakelock_plus 1.7.0, vibration 3.2.0, package_info_plus 10.2.1. Ship nonepath_provider_foundation 2.6.0, audioplayers_darwin 6.5.0; §3.2 shows why neither is a real gap. Flutter's own engine ships one (see §4.2). S verifies
C3 ITSAppUsesNonExemptEncryption in Info.plist M Read ios/Runner/Info.plist end-to-end (70 lines): the key is absent. §5 has the exact entry. S
C4 iOS app icon is the product's artwork M ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png was opened and read as an image: it is the stock Flutter logo, not Cadence. All 15 icon files are the Flutter template set. Cause: flutter_launcher_icons: is configured ios: false (pubspec.yaml:35) S
C5 1024×1024 marketing icon, no alpha P (mechanically) sips -g pixelWidth -g pixelHeight -g hasAlpha on Icon-App-1024x1024@1x.png1024, 1024, hasAlpha: no. Dimensions and alpha are correct; the artwork is wrong (C4) S
C6 Source artwork exists to generate correct iOS icons P assets/icon/ic_legacy.png measures 1024×1024 with hasAlpha: no — a valid App Store icon source. The fix is ios: true in pubspec.yaml:35 plus dart run flutter_launcher_icons S
C7 Launch screen M All three ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage*.png measure 1×1 pixel, while ios/Runner/Base.lproj/LaunchScreen.storyboard declares <image name="LaunchImage" width="168" height="185"/>. The app launches to a blank white screen S
C8 Deployment target set P IPHONEOS_DEPLOYMENT_TARGET = 13.0project.pbxproj:363 S
C9 Device family covers iPhone and iPad P TARGETED_DEVICE_FAMILY = "1,2"project.pbxproj:367. This is what makes the 13" iPad screenshot set mandatory (E3) S
C10 Version and build number wired to pubspec P CFBundleShortVersionString = $(FLUTTER_BUILD_NAME), CFBundleVersion = $(FLUTTER_BUILD_NUMBER)Info.plist:21-26, driven by version: 0.4.12+18 (pubspec.yaml:5). A regression test already pins kAppVersion to pubspec (test/version_test.dart, per lib/main.dart:12-19) S
C11 Display name set P CFBundleDisplayName = CadenceInfo.plist:9-10 S
C12 Orientations declared for both device classes P iPhone: portrait + both landscapes (Info.plist:56-61); iPad: all four (:62-68) S
C13 Scene manifest present (required by the modern Flutter template) P UIApplicationSceneManifest with $(PRODUCT_MODULE_NAME).SceneDelegateInfo.plist:29-49; ios/Runner/SceneDelegate.swift exists S
C14 UIBackgroundModes M Absent from Info.plist. See 04_ios_blockers.md §3.2 — this is a decision, not an automatic add: declaring audio to keep a timer alive is a Guideline 2.5.4 exposure S + H
C15 No tracking, no ATT prompt needed P NSUserTrackingUsageDescription absent, correctly: zero network symbols in lib/ (proof/01_findings/S13/network_grep_lib.txt, grep exit=1), zero analytics/crash packages in pubspec.yaml/pubspec.lock (same file, grep exit=1) S
C16 Custom alarm sound reachable by UNNotificationSound M The 15 tones are Flutter assets (pubspec.yaml:44-46), which land in App.framework/flutter_assets/, not the bundle root Apple requires. Detail and fix: 04_ios_blockers.md §3.5 S
C17 Sound files meet Apple's format and length rules P Measured with afinfo: all 16 WAVs are 1 ch / 44100 Hz / Int16 (Linear PCM), longest assets/audio/cascade.wav at 4.16 s, against Apple's "less than 30 seconds" and Linear-PCM-in-wav allowance (capture un_notification_sound.txt) S
C18 The app can ring when backgrounded or locked on iOS M — BLOCKER Proven by execution, not only by reading. proof/02_store/probe_ios_backstop.txt runs Backstop.init() under debugDefaultTargetPlatformOverride = TargetPlatform.iOS with the plugin's method channel mocked, and records IOS_BACKSTOP_READY=false CALLS=[] against the Android control ANDROID_BACKSTOP_READY=true CALLS=[cancelAll]. Full proof: 04_ios_blockers.md §2. S

D. App Store Connect record and metadata

Everything in this bucket is MISSING for the same root reason — no App Store Connect record exists — but each is listed because each is a distinct thing someone must produce. "Required" below means Apple's own required-properties table marks it Required (capture asc_ref_required_properties.html, parsed from the table markup, HTTP 200, retrieved 2026-08-04).

# Item Required by Apple State Notes and evidence Owner
D1 App Name (2–30 characters) Yes M "App names must be limited to 30 characters" — Guideline 2.3.7, proof/03_market/captures/policy_apple_review_guidelines.txt:170. "Cadence" is already taken on the App Store by Cadence (com.shoasano.scheduleapp, seller Sho Asano, id 6767622803) — established by stream 03 at research/_raw_03_store_name_search.md:12-16. A different App Store name is required. H
D2 Subtitle (≤30 characters) No M asc_ref_app_information.txt"This can't be longer than 30 characters." H
D3 Description Yes (version metadata) M H
D4 Keywords Yes (version metadata) M Guideline 2.3.7 bans keyword stuffing with trademarked or irrelevant terms H
D5 Primary Category Yes M H
D6 Secondary Category No M H
D7 SKU Yes M "You can't change the SKU after you add the app to your account."asc_ref_app_information.txt. IRREVERSIBLE. H
D8 Primary Language Yes M The app ships FR + EN (lib/i18n.dart:39-132); the store listing must pick a primary and ideally localise both H
D9 Bundle ID selected in the record Yes M "The Bundle ID property must match the bundle ID you set in the Xcode project" and "You can't change this property after you upload a build."asc_ref_app_information.txt. IRREVERSIBLE. H
D10 Content Rights declaration Yes M "Apps that contain, show, or access third-party content must have all the necessary rights to that content."asc_ref_app_information.txt. Relevant here: 7 TTF font files and 16 WAVs ship with zero tracked licence files (AGENT_RULES.md baseline). Stream 11's findings/S11_LICENSES_draft.md is the input for this answer. H + S
D11 Age Rating questionnaire completed Yes M "An Unrated app can't be published on the App Store." — https://developer.apple.com/help/app-store-connect/manage-app-information/set-an-app-age-rating, capture age_rating.txt, HTTP 200, retrieved 2026-08-04. Expected outcome for Cadence: 4+ — no objectionable content of any category, no user-generated content, no web view, no advertising. See §7. H
D12 Digital Services Act (DSA) Status Yes M Same as A7 H
D13 Support URL Yes M Marked Required in the platform-version-properties table (asc_ref_required_properties.html). No support URL exists for Cadence — the repository contains no website, and web/index.html:21 is still the unmodified Flutter template (<meta name="description" content="A new Flutter project.">). H
D14 Marketing URL No M H
D15 Privacy Policy URL Yes M "Privacy Policy URL — A URL that links to your company's privacy policy. This is required for all apps." — https://developer.apple.com/help/app-store-connect/reference/app-information/app-privacy, capture asc_ref_app_privacy.txt, HTTP 200, retrieved 2026-08-04. Guideline 5.1.1(i) additionally requires the policy "within the app in an easily accessible manner" — Cadence has no in-app privacy link. No privacy policy exists. H writes, S links
D16 User Privacy Choices URL No M Optional per the same capture H
D17 App Privacy answers (nutrition labels) published Yes M Full answer set in §6 H
D18 Export compliance answer Yes M Answered by the plist key in C3/§5, otherwise a questionnaire on every upload S then H
D19 Copyright field Yes M Required in the platform-version table H
D20 App Review Information (contact, notes, demo) Yes M Guideline 2.1(a): "Submissions to App Review … should be final versions with all necessary metadata and fully functional URLs included; placeholder text, empty websites, and other temporary content should be scrubbed before submission."policy_apple_review_guidelines.txt:152 H
D21 Version Release Settings Yes M Required in the platform-version table H

E. Store listing assets

# Item State Evidence Owner
E1 Screenshot format understood P "You can upload one to 10 screenshots in .jpeg, .jpg, and .png formats … Images can't include alpha channels or transparencies." — https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications, capture screenshot_specs.{txt,html}, HTTP 200, retrieved 2026-08-04
E2 Screenshots for 6.9" iPhone M Accepted sizes: 1260 × 2736 / 2736 × 1260, 1290 × 2796 / 2796 × 1290, 1320 × 2868 / 2868 × 1320. The 6.5" set (1284 × 2778 / 2778 × 1284, 1242 × 2688 / 2688 × 1242) carries the requirement line "Required if app runs on iPhone and screenshots for 6.9" display aren't provided." Since TARGETED_DEVICE_FAMILY = "1,2" includes iPhone, one of these two sets is mandatory. S produces, H curates
E3 Screenshots for 13" iPad M Accepted sizes: 2064 × 2752 / 2752 × 2064, or 2048 × 2732 / 2732 × 2048. Requirement line, verbatim: "Required if app runs on iPad." This is the mandatory set for an iPad-first product. Cadence is landscape-first on a kitchen tablet, so the landscape orientations are the ones to shoot. S produces, H curates
E4 Screenshots show the app in use M Guideline 2.3.3: "Screenshots should show the app in use, and not merely the title art, login page, or splash screen."policy_apple_review_guidelines.txt:166 H
E5 Screenshot content is 4+ appropriate P (by nature) Guideline 2.3.8: "Metadata should be appropriate for all audiences … adhere to a 4+ age rating." A timer board with dish names satisfies this. One caution: the seeded timers are a real pilot kitchen's service set — Manouche, Mozzarella sticks, Fries, Crispy, Melt cheese, Dough, chained Cook chicken (lib/engine/store.dart:326-343) — dish names, not a restaurant identifier, and test/source_hygiene_test.dart enforces that no restaurant name appears H
E6 App preview videos M (optional) Spec captured at app_previews_specs.txt; not required for submission H

F. App Review Guidelines exposure

# Guideline Risk to Cadence State Mitigation
F1 4.3(b) Spam The highest metadata risk this app faces. Verbatim: "Certain kinds of apps, such as dating, flashlight, sound effects, wallpaper, simple timers, and fortune telling, are well established on the App Store and we will not accept new submissions unless they offer a meaningfully different or improved experience."policy_apple_review_guidelines.txt:316. Apple names timers explicitly. M — nothing in the product currently states the differentiator to a reviewer Lead the description and the Notes for Review with what a consumer timer cannot do: a multi-timer board sized for a pass, chained multi-step timers (lib/engine/models.dart steps), per-timer spoken announcements in French and English (lib/audio/voice.dart, lib/i18n.dart), an alarm-stream ringtone set, an operator-visible readiness banner for wake-lock/audio/voice/notification failure (lib/diagnostics.dart:26), and an on-device flight recorder with 3 MB rotation (lib/journal.dart:25-26). Screenshots must show the multi-timer board mid-service, not one timer.
F2 4.2 Minimum Functionality "If your app is not particularly useful, unique, or 'app-like,' it doesn't belong on the App Store.":298. Lower risk than 4.3(b): Cadence is a native multi-screen app with persistence and audio, not a repackaged website. P No action beyond F1.
F3 2.5.4 Background services "Multitasking apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc.":196. Local notifications are named as a legitimate purpose. A silent keep-alive audio stream declared as UIBackgroundModes: audio is not. P today (no background modes declared at all) Fix the alarm with local notifications (04_ios_blockers.md §5), not with a background audio stream. If audio is ever declared, it must be for audio that actually plays.
F4 2.3.1 / 2.3 Accurate metadata "make sure all your app metadata, including privacy information, your app description, screenshots, and previews accurately reflect the app's core experience":160. Concrete exposure: do not describe iOS Cadence as ringing in the background until it does. Today it cannot (C18). P (no metadata written yet) Write the description after the alarm repair, and never before.
F5 2.1 App Completeness "We will reject incomplete app bundles and binaries that crash or exhibit obvious technical problems.":152. A stock Flutter icon (C4), a 1×1 launch image (C7), and a permanent ⚠️ Backup alarm unavailable operator banner (lib/i18n.dart:124, raised by alarm_backstop.dart:95) are exactly what a reviewer sees first. M Fix C4, C7 and C18 before any submission.
F6 5.1.1(i) Privacy policy "All apps must include a link to their privacy policy in the App Store Connect metadata field and within the app in an easily accessible manner.":400 M D15, plus an in-app link. Note this is the one place a code change is needed for a privacy item.
F7 Accessibility The app has zero accessibility semantics: grep -rnE "Semantics\|semanticLabel\|excludeSemantics\|Tooltip\|tooltip" lib --include='*.dart' returns 0 matches across 4,853 lines of Dart in lib/. Every control is a bare GestureDetector — the tile body at lib/ui/tile.dart:372, the inner surface at :427, and the shared _CtlBtn at :679, which is what the +10 / −10 / controls at :604, :613 and :620 route through. Every label is a glyph string: 'edit': '✎ Éditer', 'done': '✓ Terminé', 'new': '+ Nouveau' (lib/i18n.dart:41-43), and the stop control's entire label is the character '✕' (tile.dart:619). VoiceOver would announce the glyph. M Accessibility is not an App Review rejection criterion — it appears nowhere in the guidelines capture (94,514 bytes, searched). It is a product-quality and, in some jurisdictions, a procurement problem, and it is the largest decision the v2 JavaScript rewrite made that the Flutter line did not carry over (research/01_prior_work.md §2.4). Treat as HIGH product debt, not a submission blocker. Wrapping the tile and modal gesture surfaces in Semantics(button: true, label: …) is mechanical.

3. Privacy manifest — what Apple requires, and what Cadence's dependency set actually touches

3.1 The rule and its dates

"A privacy manifest is a property list file (PrivacyInfo.xcprivacy) that you add to your target's resources. … App Store Connect rejects app submissions that include invalid privacy manifest files.Starting February 12, 2025, apps you submit for review in App Store Connect must contain a valid privacy manifest file for a certain number of commonly used third-party SDKs."

Adding a privacy manifest to your app or third-party SDK, https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk (capture privacy_adding_manifest.txt, HTTP 200, retrieved 2026-08-04)

"Starting May 1, 2024, apps that don't describe their use of required reason API in their privacy manifest file aren't accepted by App Store Connect."

Describing use of required reason API, https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api (capture privacy_required_reason_api.txt, HTTP 200, retrieved 2026-08-04)

The four top-level keys are NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, NSPrivacyAccessedAPITypes (Privacy manifest files, https://developer.apple.com/documentation/bundleresources/privacy-manifest-files, capture privacy_manifest_files.txt).

3.2 Do third-party SDKs need their own manifest and signature? Yes, for a named list — and Cadence is on it

"You must include the privacy manifest for any SDK listed below when you submit new apps in App Store Connect that include those SDKs … Signatures are also required in these cases where the listed SDKs are used as binary dependencies. Any version of a listed SDK, as well as any SDKs that repackage those on the list, are included in the requirement."

Third-party SDK requirements, https://developer.apple.com/support/third-party-SDK-requirements/ (capture privacy_third_party_sdk.txt, HTTP 200, retrieved 2026-08-04)

Apple's list, cross-referenced against the nine plugins Cadence actually links on iOS (.flutter-plugins-dependencies, parsed today) and against find ~/.pub-cache -name "*.xcprivacy":

Cadence iOS plugin On Apple's named list? Ships PrivacyInfo.xcprivacy? What it declares
Flutter (the engine itself) Yes (Flutter) Yes…/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/PrivacyInfo.xcprivacy, and the xcframework carries a _CodeSignature FileTimestamp0A2A.1, C617.1; SystemBootTime35F9.1
flutter_local_notifications 22.1.0 Yes Yes UserDefaultsCA92.1
device_info_plus 13.2.0 Yes Yes all four arrays empty, NSPrivacyTracking false
share_plus 13.3.0 Yes Yes all empty
shared_preferences_foundation 2.5.6 Yes (as shared_preferences_ios) Yes UserDefaults1C8F.1
wakelock_plus 1.7.0 Yes (as wakelock) Yes all empty
package_info_plus 10.2.1 (pulled in by wakelock_plus) Yes Yes all empty
vibration 3.2.0 No Yes (all empty)
path_provider_foundation 2.6.0 Yes (as path_provider / path_provider_ios) No
audioplayers_darwin 6.5.0 No No

The one apparent gap resolves, and here is why — do not "fix" it by inventing a manifest. path_provider_foundation 2.6.0 has no native iOS target at all. Its pubspec.yaml declares ios: dartPluginClass: PathProviderFoundation and depends on ffi and objective_c; the package contains only lib/, test/, tool/ and example/ — no ios/ or darwin/ directory (ls -a on the package root, verified today). It is a pure-Dart FFI implementation whose code executes inside the app's own executable, so Apple's rule — "For each executable or dynamic library in an app that uses a required reason API, the bundle that includes the executable or dynamic library needs to include a privacy manifest file that reports the API" — points at Runner.app/PrivacyInfo.xcprivacy, which §4 supplies. It also touches no required-reason API category: NSSearchPathForDirectoriesInDomains is not one of the five (file timestamp, system boot time, disk space, active keyboard, user defaults — https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype, capture rr_api_type.txt).

audioplayers_darwin is not on Apple's list and uses AVFoundation only.

Actionable conclusion: no third-party manifest is missing. The app-level manifest is.


4. PrivacyInfo.xcprivacy — the exact file Phase 4 should commit

4.1 Which required-reason categories Cadence's own code touches

There are five categories. Cadence's position on each, established by reading its source:

Category Does Cadence's own code reach it? Evidence
NSPrivacyAccessedAPICategoryUserDefaults Yes — every persisted setting goes through shared_preferences, whose Darwin implementation is native Swift static-linked into the Runner executable. Eleven keys were observed in a real session probe: cadence-timers-v1, cadence-run-v1, cadence-clones-v1, cadence-clones-v1.corrupt, cadence-lang, cadence-vol, cadence-seeded-v1, cadence-journal-beat, cadence-journal-clean, cadence-phrase-repair-v1, cadence-zone-sound-v1 (proof/01_findings/S13/probe_data.txt) lib/engine/store.dart, lib/journal.dart:29
NSPrivacyAccessedAPICategoryFileTimestamp Yes — the flight recorder checks existence and size on every boot and rotates above 3 MB lib/journal.dart:71-72 (await f.exists(), await f.length() > _maxBytes), :25-26
NSPrivacyAccessedAPICategorySystemBootTime No — the app uses wall-clock time only. grep -rn "Stopwatch\|elapsedTicks" lib → 0 matches; 16 uses of DateTime.now(). The Flutter engine declares 35F9.1 for its own frame scheduling, in its own manifest verified today
NSPrivacyAccessedAPICategoryDiskSpace No — no free-space or total-space query anywhere in lib/ verified today
NSPrivacyAccessedAPICategoryActiveKeyboards No verified today

The two approved reason codes, verbatim from https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons (capture rr_api_reasons.txt, HTTP 200, retrieved 2026-08-04):

CA92.1 — "Declare this reason to access user defaults to read and write information that is only accessible to the app itself. This reason does not permit reading information that was written by other apps or the system, or writing information that can be accessed by other apps."

C617.1 — "Declare this reason to access the timestamps, size, or other metadata of files inside the app container, app group container, or the app's CloudKit container."

Both descriptions match Cadence exactly: it has no App Group (so 1C8F.1 would be wrong), and its journal file lives in getApplicationDocumentsDirectory() — inside the app container (lib/journal.dart:69-70).

4.2 The file

Commit as ios/Runner/PrivacyInfo.xcprivacy and add it to the Runner target's Copy Bundle Resources build phase, so it lands at Runner.app/PrivacyInfo.xcprivacy — the location Apple specifies for an iOS app (privacy_adding_manifest.txt).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSPrivacyTracking</key>
    <false/>
    <key>NSPrivacyTrackingDomains</key>
    <array/>
    <key>NSPrivacyCollectedDataTypes</key>
    <array/>
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>CA92.1</string>
            </array>
        </dict>
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>C617.1</string>
            </array>
        </dict>
    </array>
</dict>
</plist>

NSPrivacyCollectedDataTypes is empty, and that is the correct answer, not a shortcut: Apple defines collection as transmission off the device — "'Collect' refers to transmitting data off the device in a way that allows you and/or your third-party partners to access it for a period longer than what is necessary to service the transmitted request in real time" (https://developer.apple.com/app-store/app-privacy-details/, capture privacy_app_privacy_details.txt) — and Cadence transmits nothing. NSPrivacyTracking is false for the same reason. Evidence in §6.

Do not add categories speculatively. Apple: "Your app or third-party SDK must declare one or more approved reasons that accurately reflect your use of each of these APIs … You may use these APIs and the data derived from their use for the declared reasons only." (privacy_required_reason_api.txt.) An over-declared category is an inaccurate declaration.


5. ITSAppUsesNonExemptEncryption — what it is and the exact plist entry

What it is. A Boolean in Info.plist that pre-answers Apple's US export-control questionnaire.

"If you don't have the ITSAppUsesNonExemptEncryption key in your app's Info.plist file, App Store Connect walks you through an export compliance questionnaire every time you upload a new version of your app. Including the key streamlines the app submission process."

ITSAppUsesNonExemptEncryption, https://developer.apple.com/documentation/bundleresources/information-property-list/itsappusesnonexemptencryption (capture itsappusesnonexemptencryption.txt, HTTP 200, retrieved 2026-08-04)

Why its absence matters. It is not merely a nuisance. Every build uploaded to TestFlight or App Store Connect stalls in "Missing Compliance" until a human answers the questionnaire in the web UI — which, on a product where Serge builds and the project owner submits, is a hand-off on every single build.

What value Cadence needs, and why.

"Set the value for this key to NO … to indicate that your app—including any third-party libraries you link against—either uses no encryption, or only uses encryption that's exempt from export compliance requirements."

— same capture

And Apple's own documentation table for what needs paperwork:

Encryption algorithm in use Required documentation
"Your app uses encryption limited to that within the Apple operating system" "No documentation required in App Store Connect."
"Your app uses an industry standard algorithm, not provided within the Apple operating system" "Upload your French encryption declaration in App Store Connect."
"Your app uses proprietary encryption algorithms not accepted by international standard bodies" CCATS + French encryption declaration

— https://developer.apple.com/help/app-store-connect/reference/app-information/export-compliance-documentation-for-encryption (capture asc_ref_export_compliance.txt, HTTP 200, retrieved 2026-08-04)

Cadence uses no encryption of any kind. It makes no network connection: grep -rnE "package:http|HttpClient|HttpRequest|InternetAddress|WebSocket|Socket\(|Uri\.parse|Uri\.https" lib/ returns nothing (grep exit 1, proof/01_findings/S13/network_grep_lib.txt), and the three transitively reachable package:http call sites (in audioplayers, timezone/standalone.dart, timezone/browser.dart) are all unreachable from this app's import graph (proof/01_findings/S13/transitive_http_reachability.txt). There is no URLSession, no HTTPS, no crypto library. NO is therefore the accurate answer, not a convenient one.

The exact entry, to be added to ios/Runner/Info.plist:

    <key>ITSAppUsesNonExemptEncryption</key>
    <false/>

Note the caveat Apple attaches, which the project owner should be aware of even though it does not change the plist value: "If your app uses exempt forms of encryption, you might alternatively be required to submit a year-end self-classification report to the U.S. government." (capture export_compliance_docs.txt.) An app using no encryption at all has nothing to classify.


6. Privacy nutrition labels — every App Store Connect question and Cadence's answer

Reconciliation note, as instructed. findings/S13_data.md does not exist at the time of writing (ls findings/S1_engine.md, S2_persistence.md, S5_error_handling.md, S10_product.md, S11_LICENSES_draft.md). Stream 13's raw proof does exist, at proof/01_findings/S13/, and the answers below are derived from it plus my own reading of the code. When findings/S13_data.md lands, this section must be reconciled against it.

6.1 The gate question

App Store Connect's flow: "In the dialog that appears, indicate whether you or your third-party partners collect data from your app. If the answer is no, select 'No, we do not collect data from this app' and then click Save. You don't need to answer any further questions." — https://developer.apple.com/help/app-store-connect/manage-app-information/manage-app-privacy (capture privacy_policy_url.txt, HTTP 200, retrieved 2026-08-04)

Cadence's answer: "No, we do not collect data from this app."

6.2 Why that answer is correct, with proof

Apple's definition: "'Collect' refers to transmitting data off the device in a way that allows you and/or your third-party partners to access it for a period longer than what is necessary to service the transmitted request in real time. 'Third-party partners' refers to analytics tools, advertising networks, third-party SDKs, or other external vendors whose code you've added to your app." (capture privacy_app_privacy_details.txt.)

Test Result Evidence
Any network symbol in lib/? None grep -rnE "package:http\|HttpClient\|HttpRequest\|InternetAddress\|WebSocket\|Socket(\|RawDatagramSocket\|package:dio\|XMLHttpRequest\|Uri\.parse\|Uri\.https\|Uri\.http" lib/ → grep exit 1. proof/01_findings/S13/network_grep_lib.txt
Any analytics / crash / ads package? None grep -niE "firebase\|crashlytics\|sentry\|analytics\|amplitude\|mixpanel\|posthog\|bugsnag\|appcenter\|datadog\|google_mobile_ads\|facebook" pubspec.yaml pubspec.lock → grep exit 1. Same file
Transitive network reachable? No The three package:http sites in the dependency graph are in audioplayers/lib/src/audio_cache.dart (inside if (kIsWeb)), timezone/lib/standalone.dart, timezone/lib/browser.dart. Cadence imports timezone/data/latest.dart and timezone/timezone.dart only (lib/alarm_backstop.dart:20-21) and plays only AssetSource (lib/audio/audio.dart:73), never UrlSource. proof/01_findings/S13/transitive_http_reachability.txt
Where does data actually live? On device only 11 SharedPreferences keys (→ NSUserDefaults on iOS) and one file, <app documents>/cadence-journal.txt, 3 MB rotating. proof/01_findings/S13/probe_data.txt
Does anything identifying get written? Device model string only, on device lib/journal.dart:123-127 writes '${i.name} ${i.model} · iOS ${i.systemVersion}' into the journal file. It never leaves the device on its own.
Can that leave the device? Only if the operator explicitly shares it lib/journal.dart:205,224-225 writes a named copy to the temporary directory and lib/ui/modals.dart:706 hands it to the system share sheet. That is user-initiated export through share_plus, not developer collection — the data is not transmitted to Serge or the project owner, and no server receives it.

6.3 The full answer sheet, question by question

If Apple's flow or a future reviewer requires the itemised form rather than the gate answer, these are the answers. Every one is Not Collected.

App Store Connect data category Sub-types Cadence answer Why
Contact Info Name, Email Address, Phone Number, Physical Address, Other User Contact Info Not Collected No account, no login, no contact field anywhere in lib/
Health & Fitness Health, Fitness Not Collected No HealthKit, no sensors
Financial Info Payment Info, Credit Info, Other Financial Info Not Collected No in-app purchase code, no billing (R6 keeps billing out of scope; none exists today)
Location Precise Location, Coarse Location Not Collected No location plugin in pubspec.yaml; no NS*LocationUsageDescription in Info.plist
Sensitive Info Sensitive Info Not Collected
Contacts Contacts Not Collected No contacts plugin
User Content Emails or Text Messages, Photos or Videos, Audio Data, Gameplay Content, Customer Support, Other User Content Not Collected Timer names and spoken phrases are user content, but they are stored in NSUserDefaults on the device and never transmitted
Browsing History Browsing History Not Collected No web view; webview_flutter is not a dependency
Search History Search History Not Collected No search feature
Identifiers User ID, Device ID Not Collected Timer IDs are locally generated random strings (lib/engine/models.dart); the device model string in the journal is not an identifier and is not transmitted
Purchases Purchase History Not Collected
Usage Data Product Interaction, Advertising Data, Other Usage Data Not Collected The journal records product interaction, on device, for the operator's own diagnosis. Never transmitted
Diagnostics Crash Data, Performance Data, Other Diagnostic Data Not Collected lib/diagnostics.dart is a 50-entry in-memory ring buffer; lib/journal.dart is a local file. No crash reporter
Other Data Other Data Types Not Collected
Tracking (used with third-party data for advertising) No No advertising SDK, no third-party data, no NSUserTrackingUsageDescription, no ATT prompt

One thing to keep honest as the product grows. The moment any billing, licence check, crash reporter, or remote-config call is added, this entire sheet changes and both the nutrition labels and PrivacyInfo.xcprivacy must be revised. Apple: "You're responsible for keeping your responses accurate and up to date." (capture privacy_policy_url.txt.)


7. Age rating, content rights, and the two URLs that do not exist

Age rating. Required; an unrated app cannot ship: "An Unrated app can't be published on the App Store." (capture age_rating.txt.) The questionnaire is completed in App Store Connect under App Information → Set Up Age Ratings. Cadence's expected outcome is 4+: no violence, no sexual content, no profanity, no gambling, no alcohol/tobacco/drug references, no horror, no user-generated content, no unrestricted web access, no advertising, no in-app purchases. Verified against the code, not assumed — lib/i18n.dart holds every user-visible string in FR and EN, and test/source_hygiene_test.dart already forbids restaurant identifiers in source. Do not guess at the questionnaire: Guideline 2.3.6 warns that a mis-rated app "could trigger an inquiry from government regulators" (policy_apple_review_guidelines.txt:169).

Content rights. A required App-information property. Apple: "Apps that contain, show, or access third-party content must have all the necessary rights to that content or be otherwise permitted to use it under the laws of each App Store country or region in which they're available." (capture asc_ref_app_information.txt.) The relevant exposure for Cadence is recorded in the audit baseline: zero tracked licence files for 7 TTF fonts and 16 WAVs (AGENT_RULES.md). Stream 11 has drafted the licence inventory at findings/S11_LICENSES_draft.md; that draft is the input to this answer. Answering "yes, I have the rights" without it is an unverified declaration.

Support URL — required, and none exists. Apple's required-properties table marks Support URL Required (asc_ref_required_properties.html, parsed). The repository contains no website; the only web artifact is web/index.html, still the unmodified Flutter template with <meta name="description" content="A new Flutter project."> at :21 and <title>cadence</title> at :32, and web/manifest.json:2-7 still carrying "description": "A new Flutter project." and Flutter-blue #0175C2 theme colours. Guideline 2.1(a) explicitly bans "empty websites" in a submission. Someone must stand up a real support page before submission.

Privacy policy URL — required, and none exists. "Privacy Policy URL — A URL that links to your company's privacy policy. This is required for all apps." (capture asc_ref_app_privacy.txt.) Guideline 5.1.1(i) additionally requires the policy to be reachable "within the app in an easily accessible manner" and to "clearly and explicitly … Identify what data, if any, the app/service collects, how it collects that data, and all uses of that data" and "Explain its data retention/deletion policies" (policy_apple_review_guidelines.txt:400). Cadence has no in-app privacy link — this is the one privacy item that requires a code change, not just a URL. The policy's content is short and easy, because §6 establishes that the honest answer is "this app collects nothing and transmits nothing"; what it must additionally state is that the on-device journal exists, what it contains, that it rotates at 3 MB, and that the operator can export or delete it.


8. Icons and listing assets — the concrete numbers

The iOS icon is the stock Flutter logo. This is not an inference from ios: false; the 1024×1024 file was opened and viewed. All 15 files in ios/Runner/Assets.xcassets/AppIcon.appiconset/ are the Flutter template set. Meanwhile Android is correctly branded — flutter_launcher_icons generated mipmap-{m,h,xh,xxh,xxxh}dpi/ic_launcher.png plus an adaptive mipmap-anydpi-v26/ic_launcher.xml, because android: true (pubspec.yaml:34). Only ios: false (:35) left iOS behind.

The fix is two lines and one command, and the source artwork is already in the repository at assets/icon/ic_legacy.png (1024×1024, hasAlpha: no):

flutter_launcher_icons:
  android: true
  ios: true          # was: false

then dart run flutter_launcher_icons.

Apple's icon rules are in the Human Interface Guidelines (capture hig_app_icons.txt, HTTP 200, retrieved 2026-08-04); the App Store Connect mechanical constraint that already holds is 1024×1024 with no alpha channel.

Screenshots — the mandatory sets, precise pixels. From https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications (capture screenshot_specs.{txt,html,png}, HTTP 200, retrieved 2026-08-04). One to ten per set, .jpeg/.jpg/.png, no alpha channel.

Device class Requirement line, verbatim Accepted sizes (portrait / landscape)
iPad 13" — iPad Pro (M5, M4), iPad Pro 6th–1st gen, iPad Air (M4, M3, M2) "Required if app runs on iPad" 2064 × 2752 / 2752 × 2064, or 2048 × 2732 / 2732 × 2048
iPhone 6.9" — iPhone Air, 17 Pro Max, 16 Pro Max, 16 Plus, 15 Pro Max, 15 Plus, 14 Pro Max (primary iPhone set) 1260 × 2736 / 2736 × 1260, 1290 × 2796 / 2796 × 1290, 1320 × 2868 / 2868 × 1320
iPhone 6.5" — 14 Plus, 13 Pro Max, 12 Pro Max, 11 Pro Max, 11, XS Max, XR "Required if app runs on iPhone and screenshots for 6.9" display aren't provided" 1284 × 2778 / 2778 × 1284, 1242 × 2688 / 2688 × 1242
iPad 11", 12.9", and all smaller iPhone classes "If screenshots with the accepted sizes aren't provided, scaled screenshots … are used." not required

For Cadence specifically: TARGETED_DEVICE_FAMILY = "1,2" (project.pbxproj:367) means the app runs on both, so both the 13" iPad set and one iPhone set are mandatory. The iPad set is the one that sells the product — Cadence is a landscape kitchen board, and grid_layout_test.dart pins its geometry at 1280×740 and 800×540 — so shoot the landscape iPad orientations (2752 × 2064 or 2732 × 2048) showing a full board mid-service, which also answers Guideline 2.3.3 and supports the 4.3(b) differentiation argument in F1.


9. Signing and distribution — the concrete sequence for the project owner on a Mac with Xcode

Nothing in this sequence can be executed on this machine. Steps 0–4 are prerequisites the project owner owns; steps 5–10 are the build and upload.

0.  Decide individual vs organisation (A2). If organisation: obtain the legal entity,
    then request a D-U-N-S number and wait up to 5 + 2 business days (A3).

1.  Enrol in the Apple Developer Program at https://developer.apple.com/programs/enroll/
    and pay the 99 USD annual fee.  Capture: adp_programs_enroll.txt

2.  Install Xcode from the Mac App Store, then:
        sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
        xcodebuild -version          # must be 26.x if AlarmKit is ever used
        flutter doctor -v            # the iOS toolchain row must go green

3.  Register the App ID  dev.sergemio.cadence  in Certificates, Identifiers & Profiles.
    IRREVERSIBLE — see §10.  Capture: register_app_id.txt

4.  Create the App Store Connect app record: name, primary language, bundle ID,
    SKU.  Capture: asc_add_new_app.txt

5.  In Xcode, open ios/Runner.xcworkspace, select the Runner target, Signing &
    Capabilities: set the Team (this writes DEVELOPMENT_TEAM into project.pbxproj,
    B3), leave "Automatically manage signing" on so Xcode issues the Apple
    Distribution certificate and the App Store provisioning profile (B4, B5).
    Add the "Time Sensitive Notifications" capability — this creates
    Runner.entitlements and sets CODE_SIGN_ENTITLEMENTS (B6).

6.  Add ios/Runner/PrivacyInfo.xcprivacy (§4) to the Runner target's Copy Bundle
    Resources phase, and ios/Runner/cadence_alarm.wav likewise
    (04_ios_blockers.md §5, B3).

7.  First real compile — expect this to surface errors, because no iOS build has
    ever run:
        flutter clean
        flutter pub get
        flutter build ios --release --no-codesign     # compile check only

8.  Archive and export:
        flutter build ipa --release
    or, from Xcode: Product > Archive, then Distribute App > App Store Connect.

9.  Upload:
        xcrun altool --upload-app -f build/ios/ipa/*.ipa -t ios \
                     --apiKey <KEY_ID> --apiIssuer <ISSUER_ID>
    (or Xcode Organizer > Distribute).  Capture: upload_build.txt

10. TestFlight internal test on the real kitchen iPad BEFORE any App Store
    submission, and run the verdict-flipping alarm test from
    04_ios_blockers.md §5 on that build.  Capture: testflight_overview.txt

Guideline 2.2 is explicit that TestFlight is the right home for this stage: "Demos, betas, and trial versions of your app don't belong on the App Store – use TestFlight instead." (policy_apple_review_guidelines.txt:156.)


10. Ordered action list

Ordering is by dependency, then by whether the item blocks a submission. "Irreversible" means it cannot be undone without a new app record, a new account, or an Apple support ticket.

Irreversible decisions — settle these before anything else

# Action Owner Depends on Why irreversible
I1 Decide individual vs organisation enrolment. the project owner An individual account publishes Serge's legal name as the seller, permanently, on every product page worldwide. Converting to an organisation is a support ticket, not a setting: "please contact us" (adp_enrollment.txt:146).
I2 Fix the bundle identifier. dev.sergemio.cadence is already in the project (project.pbxproj:385); the question is whether it is the right one for a product sold under a business identity. the project owner + Serge I1 Once registered as an App ID and once a build is uploaded, it cannot be changed: "You can't change this property after you upload a build" (asc_ref_app_information.txt). A different bundle ID later means a new app record, a new App Store listing, and no upgrade path for installed users. If the entity may become a company, decide now whether the reverse-DNS should be the company's rather than dev.sergemio.
I3 Choose the App Store name. "Cadence" is taken by another developer (stream 03, research/_raw_03_store_name_search.md:12-16). the project owner I2 The name is granted to one app per localisation.
I4 Choose the SKU. the project owner I2 "You can't change the SKU after you add the app to your account" (asc_ref_app_information.txt).

Blocking — a submission cannot be accepted without these

# Action Owner Depends on Item
B1 Enrol in the Apple Developer Program and pay 99 USD/yr. the project owner I1 (+ D-U-N-S if organisation, 5 + 2 business days) A1
B2 Repair the iOS alarm. Add DarwinInitializationSettings and DarwinNotificationDetails, ship the alarm WAV at the bundle root, add the Time Sensitive Notifications capability. Full spec: 04_ios_blockers.md §5. Serge C18, C16, B6
B3 Generate the real iOS app icons: ios: true in pubspec.yaml:35, dart run flutter_launcher_icons. Serge C4
B4 Commit ios/Runner/PrivacyInfo.xcprivacy (exact XML in §4.2) and add it to Copy Bundle Resources. Serge C1
B5 Add ITSAppUsesNonExemptEncryption = false to ios/Runner/Info.plist (§5). Serge C3, D18
B6 Install Xcode; produce the first iOS build that has ever existed; fix whatever it surfaces. the project owner (machine) + Serge (fixes) B1 B8
B7 Register the App ID, set the Team, obtain the distribution certificate and App Store provisioning profile. the project owner B1, B6, I2 B2–B5
B8 Create the App Store Connect record. the project owner B7, I3, I4 B7
B9 Publish a real privacy policy at a stable URL, and add an in-app link to it (Guideline 5.1.1(i) requires both). Content is short — §6 proves the app collects nothing. the project owner writes, Serge links D15, F6
B10 Publish a real support page at a stable URL. Guideline 2.1(a) bans empty websites. the project owner D13
B11 Declare DSA trader status and supply the address (or documented P.O. Box), phone and email Apple will publish on every EU product page. the project owner B8 A7, A8
B12 Complete the age rating questionnaire (expected 4+). the project owner B8 D11
B13 Answer content rights — using findings/S11_LICENSES_draft.md, not a guess. Track the licence files for the 7 fonts and 16 WAVs. the project owner + Serge S11 D10
B14 Publish the App Privacy answers: "No, we do not collect data from this app" (§6). the project owner B8 D17
B15 Produce the 13" iPad screenshots (2752 × 2064 or 2732 × 2048, landscape) and one iPhone set (6.9" or 6.5"), showing the board in use. Serge captures, the project owner curates B6 E2, E3, E4
B16 Write name, subtitle, description, keywords, category, copyright, and Notes for Review — leading with the 4.3(b) differentiation argument (§F1). the project owner I3, B15 D1–D6, D19, D20
B17 TestFlight the build on the real kitchen iPad and run the alarm verdict test (04_ios_blockers.md §5) before submitting. the project owner + Serge B6, B7

High — fix before selling, not necessarily before submitting

# Action Owner Item
H1 Replace the 1×1 pixel launch images with a real launch screen. Serge C7
H2 Decide the UIBackgroundModes question deliberately. The recommendation from §F3 and 04_ios_blockers.md §3.2 is: do not declare audio; fix the alarm with local notifications instead. Serge + the project owner C14
H3 Add accessibility semantics: wrap the tile and modal gesture surfaces in Semantics(button: true, label: …) and give every glyph-only control an accessible name. Zero exist today across 4,853 lines. Serge F7
H4 Clean the web/ shell metadata ("A new Flutter project.", Flutter-blue theme colours) — it is not shipped to the App Store but it is the same class of metadata-accuracy defect Guideline 2.3 targets, and it will be visible if the support page is ever built from it. Serge
H5 Decide whether the seeded pilot-kitchen timer set (lib/engine/store.dart:326-343) is what a new restaurant should see on first launch. the project owner E5

Medium — resolve before scaling

# Action Owner
M1 Decide the AlarmKit question: raise the deployment floor to iPadOS 26 and use AlarmKit (no Apple approval needed, overrides silent and Focus), or stay on iOS 13 and pursue the Critical Alerts entitlement. 04_ios_blockers.md §6-7. the project owner + Serge
M2 If Critical Alerts is chosen: retrieve the request form while signed in (it is behind Apple's sign-in wall — proven by capture request_critical_alerts.txt, title Sign In - Apple, 89 bytes after 3 attempts) and file the request. the project owner
M3 Add a CI workflow. find . -path '*.github*' → nothing; the 123 tests only run when a human types flutter test (research/01_prior_work.md §2.1, A1-3). Serge
M4 Reconcile §6 against findings/S13_data.md when it exists. Coordinator

11. What could not be established, and the exact artifact that would settle it

Per R3, no item here is left as "unknown" — each names the artifact and the test.

Question Why it is open The artifact that settles it
Apple's stated approval criteria for the Critical Alerts entitlement The request form redirects unauthenticated clients to Apple's sign-in wall. Our capture returned title Sign In - Apple, 89 bytes, after 3 attempts (request_critical_alerts.txt, MANIFEST_apple.md). The entitlement documentation states only "fill out the request form", and the 94,514-byte App Review Guidelines capture contains no criteria for it. The content of https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/ retrieved while signed in to an enrolled Apple Developer account. Requires B1. Do not substitute a blog post or forum thread — R11.
Whether the iOS target compiles at all No Xcode on this machine; the target has never been built; AppDelegate.swift:12-13 says so. flutter build ios --release --no-codesign on a Mac with Xcode installed.
Whether the repaired alarm actually rings backgrounded, locked, muted, and force-quit Only a device run can answer it, on either platform. Android has the same gap: commit f46d142 records verbatim that the OS backstop never actually rang in the only field log. The verdict-flipping test, specified at 04_ios_blockers.md §5: 60-second timer, device locked, external clock or video proving the audible alarm begins at 60 s without reopening the app; repeated force-quit, silent-switch-on, and Focus-on.
Whether Apple accepts the 4.3(b) differentiation argument It is a reviewer judgement, not a published test. The first submission's review outcome. Mitigation is F1, executed before submitting.

12. Coverage manifest

Every file and source consulted for this document, and what was checked in each. Repository files are at 03a176e. Files marked (iOS) are also covered in 04_ios_blockers.md §9 with different questions asked.

Source Size What was checked here
ios/Runner/Info.plist (iOS) 70 lines Read end-to-end; all 18 keys enumerated; confirmed absent: ITSAppUsesNonExemptEncryption, ITSEncryptionExportComplianceCode, UIBackgroundModes, NSUserTrackingUsageDescription, every NS*UsageDescription. Confirmed present: display name, version wiring, orientations, scene manifest.
ios/Runner.xcodeproj/project.pbxproj (iOS) Grepped every signing/identity setting. Present: PRODUCT_BUNDLE_IDENTIFIER (:385, :401), IPHONEOS_DEPLOYMENT_TARGET (:363), TARGETED_DEVICE_FAMILY (:367), legacy CODE_SIGN_IDENTITY (:349), CODE_SIGN_STYLE = Automatic on RunnerTests only (:397). Absent: DEVELOPMENT_TEAM, PROVISIONING_PROFILE_SPECIFIER, CODE_SIGN_ENTITLEMENTS, any .xcprivacy reference.
ios/ tree, whole 48 files find ios -type f. Confirmed absent: *.entitlements, *.xcprivacy, Podfile, Podfile.lock. Present: Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/ (Swift Package Manager).
ios/Runner/Assets.xcassets/AppIcon.appiconset/ 15 PNGs Every file measured with sips for dimensions and alpha; MD5 of each recorded; the 1024×1024 opened and viewed — it is the stock Flutter logo.
ios/Runner/Assets.xcassets/LaunchImage.imageset/ 3 PNGs All three measured 1×1 px; compared against LaunchScreen.storyboard's declared 168×185.
assets/icon/ 3 PNGs All 1024×1024; ic_legacy.png has no alpha and is a valid App Store icon source.
pubspec.yaml 68 lines Read end-to-end. 10 direct dependencies; flutter_launcher_icons block (:31-39) including ios: false (:35); asset (:44-46) and font (:48-68) declarations; version: 0.4.12+18 (:5).
pubspec.lock Resolved versions for every plugin cited in §3.2.
.flutter-plugins-dependencies Parsed with Python; the nine iOS plugins enumerated.
~/.pub-cache/hosted/pub.dev/ 17 .xcprivacy files find -name "*.xcprivacy" across the whole cache; each of the six directly relevant manifests read verbatim; path_provider_foundation-2.6.0 and audioplayers_darwin-6.5.0 confirmed to contain none; path_provider_foundation's pubspec.yaml and directory listing read to establish it is pure-Dart FFI.
Flutter SDK 3.44.8 engine artifacts find … -name "PrivacyInfo.xcprivacy"; the ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/PrivacyInfo.xcprivacy read verbatim; _CodeSignature presence confirmed.
lib/alarm_backstop.dart 279 lines Read end-to-end (detail in 04_ios_blockers.md §9).
lib/journal.dart File-timestamp call sites (:71-72), storage location (:69-70), rotation constants (:25-26), device string (:114-129), export path (:205,224-229), SharedPreferences handle (:29).
lib/engine/store.dart Seed set (:326-343) for the E5 metadata question.
lib/i18n.dart Glyph-only control labels (:41-43) and the backstopDown banner strings (:79, :124).
lib/ui/tile.dart, lib/ui/modals.dart Grepped for accessibility semantics (0 matches) and for the share-sheet call site (modals.dart:706).
lib/ whole tree 4,853 lines grep -rnE "Semantics\|semanticLabel\|excludeSemantics\|Tooltip\|tooltip" → 0; grep -rn "Stopwatch\|elapsedTicks" → 0; grep -rn "DateTime.now()" → 16; grep -rn "Platform\.is" → 2, both in journal.dart.
assets/audio/*.wav, android/app/src/main/res/raw/cadence_alarm.wav 16 files Each measured with afinfo for data format and duration.
web/index.html, web/manifest.json Template-metadata check for §7.
android/app/src/main/AndroidManifest.xml Permission and receiver set, for the parity table in 04_ios_blockers.md §4.
proof/01_findings/S13/ 6 files probe_data.txt (the 11 real SharedPreferences keys and the journal file), network_grep_lib.txt (zero network symbols, zero analytics packages), transitive_http_reachability.txt (the three unreachable package:http sites), device_info_reads.txt, apk_permissions.txt, no_config_export_import.txt.
research/01_prior_work.md 410 lines §1–§2.4 for prior-finding status, the verdict-flipping device test, and the accessibility decision the v2 rewrite made.
research/_raw_03_store_policy.md 526 lines §A (enrolment), §D (DSA trader) — reused rather than re-derived, per R7.
research/_raw_03_store_name_search.md 310 lines §1–2.2 — "Cadence" is taken on the App Store.
proof/03_market/captures/policy_apple_review_guidelines.txt 95,394 bytes Guidelines 2.1, 2.2, 2.3–2.3.12, 2.5.1, 2.5.4, 4.2–4.2.7, 4.3(a)(b), 5.1.1(i)–(x) read verbatim; whole file searched for "accessibility" (no requirement) and for background-mode language.
proof/02_store/captures/ 66 distinct Apple URLs Every Apple page cited above, each with .txt, .html, .png and a MANIFEST_apple.md row carrying HTTP status, byte count, attempt count and UTC retrieval time. The directory is shared with the Google Play stream (its own index is MANIFEST_google_play.md); MANIFEST_apple.md indexes only this stream's captures. Three URLs returned Apple's own "Page Not Found" and were replaced with the correct ones found by parsing the sidebar link graph in the captured HTML: export_compliance_asc, provisioning_profiles, asc_app_information. One returned Apple's sign-in wall and is reported as such in §11: request_critical_alerts.
proof/02_store/probe_ios_backstop.txt and proof/02_store/s02_probe_platform_test.dart 2 files The executed proof behind C18, recorded through proof/run_and_record.sh [not published] per R12. Run on a cp -R copy at a scratch working copy; the pinned repo is untouched (git -C Projects/cadence-app status --porcelain → 0 lines, HEAD 03a176e).
04 — iOS technical viability and the alarm-parity gap versus Androidstore_readiness/04_ios_blockers.md · raw .md

04 — iOS technical viability and the alarm-parity gap versus Android

Stream: 02 — Apple App Store readiness (Phases 2.2, 2.3, 2.4) Subject: the app repository at pinned sha 03a176e72ef0075eec86b8915cbe6e93042a3b9d, version 0.4.12+18 Written: 2026-08-04 Companion document: store_readiness/02_apple_app_store.md (the submission checklist and action list) Raw captures: proof/02_store/captures/MANIFEST_apple.md records url, final url, HTTP status, title, byte count, attempt count and UTC retrieval time for every Apple page cited here. Execution proof for §2 is at proof/02_store/probe_ios_backstop.txt with its source at proof/02_store/s02_probe_platform_test.dart.


0. Method, and the limit of this document

Xcode is not installed on this machine — Command Line Tools only, and CocoaPods is absent (proof/00_baseline/doctor.txt, AGENT_RULES.md toolchain row). No iOS binary of Cadence has ever been produced, by this audit or by Serge. The app's own iOS platform adapter says so in a header comment:

// ⚠️ NOT YET COMPILED — written on Windows, no Xcode available. Must be built
// and run on a Mac before anyone claims iOS works.

ios/Runner/AppDelegate.swift:12-13 (valid at 03a176e)

Findings are therefore of three kinds and are labelled as such throughout:

  • PROVEN BY EXECUTION (Dart layer) — the Dart half of the app can be run here, under the Flutter test binding with a platform override. §2 uses this, and it is the strongest evidence in the document. It proves what the Dart code does on iOS; it does not prove what iOS does.
  • PROVEN STATICALLY — settled by reading source that will execute deterministically. A runtime run cannot change these; it can only confirm them.
  • RUNTIME-UNVERIFIED — depends on device, OS version, power state, or Apple's runtime scheduling. Named here with the exact artifact that would settle it.

All execution happened on a cp -R copy at a scratch working copy, per R10. The pinned repo was not modified: git status --porcelain on cadence-app is empty and HEAD is still 03a176e72ef0075eec86b8915cbe6e93042a3b9d.

Nothing in this document was taken from memory. Every Apple claim carries a URL and a stored capture path.


1. The verdict, stated plainly

As the code stands at 03a176e, Cadence has no alarm of any kind on iOS when the app is not in the foreground. Not a degraded alarm — none. The mechanism that was built for exactly this purpose is dead on arrival on iOS, and it fails silently.

That is not a prediction about a device run. It was executed and recorded: with the plugin's method channel mocked identically on both platforms, Backstop.ready comes back true on Android with one channel call, and false on iOS with zero channel calls (proof/02_store/probe_ios_backstop.txt; full transcript in §2.7).

Can a professional kitchen timer meet its promise on iOS? Yes, but only in one of three configurations, and the one the product actually needs is not currently reachable:

# Configuration Rings backgrounded? Rings locked? Rings after force-quit? Overrides silent switch? Overrides Focus / Do Not Disturb? Available to Cadence today
A Foreground kiosk: iPad on mains power, app on screen, wakelock_plus holding the idle timer off; the in-app Dart engine is the alarm No — but the app is never backgrounded in this configuration No — a manual power-button lock backgrounds the app, which is then suspended (§3.1) No Yes (.playback session) Yes (audio is not a notification) Yes, once the app is compiled. This is the configuration the product is designed around.
B Configuration A plus a repaired UNTimeIntervalNotificationTrigger local-notification backstop Yes Yes Yes — the notification is held by the system, not by the app No — silent switch and mute suppress the sound unless the notification is critical No — suppressed by Focus unless timeSensitive or critical No. Requires the repair in §5.
C Configuration B plus Critical Alerts entitlement, or plus AlarmKit on iOS 26+ Yes Yes Yes Yes Yes No. Requires Apple approval (Critical Alerts) or an iOS-26 minimum and new native code (AlarmKit).

Binary answer to the question asked: No — this app cannot reliably ring on iOS when backgrounded or locked, today. To make it do so requires, at minimum, Configuration B (a repair of roughly twenty lines of Dart plus a bundle-root sound file). To make it ring through a muted or Focus-ed iPad — which is the realistic state of a tablet on a pass in service — requires Configuration C.

Is the Critical Alerts entitlement needed? Only for Configuration C, and only on iPads running earlier than iPadOS 26. From iPadOS 26 onward, AlarmKit achieves the same override — verbatim, "It overrides both a device's focus and silent mode, if necessary" — with no Apple approval step (§7). Because the product is iPad-first and a restaurant buys the tablet for the app, requiring iPadOS 26 is a live option that removes the entitlement dependency entirely.


2. PROVEN BY EXECUTION — the backstop throws on iOS and every scheduling path silently no-ops

This is the single most consequential finding of the stream. It was first established by reading three code citations that compose into a deterministic control flow (§2.1–2.4), and then confirmed by running the code (§2.7). It is not a static inference.

2.1 The app passes Android-only initialisation settings

  Future<void> init() async {
    try {
      tzdata.initializeTimeZones();
      await _plugin.initialize(
        settings: const InitializationSettings(
          android: AndroidInitializationSettings('@mipmap/ic_launcher'),
        ),
      );

lib/alarm_backstop.dart:69-76 (valid at 03a176e)

InitializationSettings is constructed with android: only. settings.iOS is null.

2.2 The plugin throws when settings.iOS is null on iOS

    } else if (defaultTargetPlatform == TargetPlatform.iOS) {
      if (settings.iOS == null) {
        throw ArgumentError(
          'iOS settings must be set when targeting iOS platform.',
        );
      }

~/.pub-cache/hosted/pub.dev/flutter_local_notifications-22.1.0/lib/src/flutter_local_notifications_plugin.dart:142-147 (flutter_local_notifications: ^22.1.0, pubspec.yaml:17; resolved to 22.1.0, pubspec.lock)

This throw happens before the plugin ever reaches the platform channel. It is unconditional on iOS. There is no device, OS version, or permission state under which it does not fire.

2.3 The throw is caught and turned into a permanently disabled module

    } catch (e) {
      // iOS / broken channel: the in-app engine still covers the foreground
      _ready = false;
      Diag.fail('backstop-init', e, isCritical: true);
    }

lib/alarm_backstop.dart:92-96

The comment shows the author knew. _ready stays false for the life of the process, and there is no retry: init() is called exactly once, from lib/ui/home.dart:149.

2.4 Every entry point is guarded on _ready

Method Guard Line
sync(engine, body) — called on every run mutation if (!_ready) return; lib/alarm_backstop.dart:123
_flushSchedules() — commits debounced re-arms if (p == null \|\| !_ready) return; :159
onBackground(engine, body) — the app leaves the screen while ringing if (!_ready) return; :239
showNow(t, body) — a timer fired while off-screen if (!_ready) { Journal.log(...); return; } :253-256
onForeground() — clear shown notifications if (!_ready) return; :269

Consequence. On iOS, Backstop.sync() is called from five sites in lib/ui/home.dart (:152, :190, :256, and onBackground at :199, showNow at :300) and every one of them returns immediately without doing anything. No UNNotificationRequest is ever created. No system alarm exists. When the iPad's home button is pressed, the timer stops existing as far as the operating system is concerned.

2.5 The failure is visible to the operator, but only as a banner

Diag.fail('backstop-init', e, isCritical: true) at :95 feeds lib/diagnostics.dart:26 ValueNotifier<Set<String>> critical, which lib/ui/home.dart:685-686 turns into the operator banner string backstopDown, whose English text is verbatim ⚠️ Backup alarm unavailable — keep the app on screen (lib/i18n.dart:124; French at :79). The banner is a full-width bar in burnt red, Color(0xFFB3452B) with white 13.5 pt semibold text (lib/ui/home.dart:690-699).

So an iOS user is told, permanently, on every launch, that the backup alarm does not work. That is honest engineering, and it is also a product that ships a permanent red failure bar across the top of the screen on the platform it is being submitted to. Severity: BLOCKER under AGENT_RULES.md R13 — "fails to ring an alarm".

2.6 A second, independent defect behind the first

Even if §2.1 were fixed by adding DarwinInitializationSettings, the notification would still be wrong, because the notification details are Android-only too:

  static const NotificationDetails _details =
      NotificationDetails(android: _channel);

lib/alarm_backstop.dart:61-62

and the plugin forwards notificationDetails.iOS — which is null — to the iOS implementation:

          ?.zonedSchedule(
            id: id,
            title: title,
            body: body,
            scheduledDate: scheduledDate,
            notificationDetails: notificationDetails.iOS,

…/flutter_local_notifications_plugin.dart:451-456

Result: default notification sound, default interruption level, no custom tone, no critical flag. Two fixes are required, not one.

2.7 The runtime proof, and why the 123-test suite never caught this

The chain above was executed, on a cp -R copy at a scratch working copy (the pinned repo was not touched — git status --porcelain on cadence-app is empty and HEAD is still 03a176e, R10 satisfied). The probe installs the plugin's mock method channel exactly the way the repo's own test/backstop_test.dart:33,47-66 does, then runs Backstop.init() under each platform override, so Android and iOS are compared on equal terms.

Recorded output, verbatim, from proof/02_store/probe_ios_backstop.txt (command flutter test test/zz_s02_probe_platform_test.dart --reporter=expanded, cwd a scratch working copy, GIT_HEAD 03a176e…, TREE_STATE: CLEAN, EXIT_CODE=0, 2026-08-04T11:15:05Z):

00:00 +0: PROBE-1 default target platform under flutter_test
DEFAULT_TARGET_PLATFORM=TargetPlatform.android
OVERRIDE=null
00:00 +1: PROBE-2 plugin initialize under iOS with android-only settings
IOS_INIT_THROWN=ArgumentError: Invalid argument(s): iOS settings must be set when targeting iOS platform.
IOS_CHANNEL_CALLS=[]
00:00 +2: PROBE-3 Backstop.ready after init() — iOS
[cadence] backstop-init: Invalid argument(s): iOS settings must be set when targeting iOS platform.
IOS_BACKSTOP_READY=false  CALLS=[]
00:00 +3: PROBE-4 Backstop.ready after init() — Android control
ANDROID_BACKSTOP_READY=true  CALLS=[cancelAll]
00:00 +4: All tests passed!

The probe source is preserved at proof/02_store/s02_probe_platform_test.dart.

Three things this settles:

  1. Backstop.ready is false on iOS and true on Android, from identical setup. The difference is the platform, nothing else.
  2. Zero method-channel calls are made on iOS (CALLS=[]) against [cancelAll] on Android. The throw happens before any platform code is reached, which is why no amount of iOS permission-granting or plist configuration would change it.
  3. defaultTargetPlatform under flutter_test is TargetPlatform.android, with no override set. That is the reason the repository's 123 passing tests — including test/backstop_test.dart, which is a real, thorough backstop test — never touched this branch. The suite is not weak; it is Android-only by default, and nobody ever pointed it at iOS.

Consequence for Phase 4: the regression test that must exist is a debugDefaultTargetPlatformOverride = TargetPlatform.iOS arm in test/backstop_test.dart asserting backstop.ready == true and a non-empty CALLS list. It goes red at 03a176e and green after the §5 repair, and it costs one setUp line.


3. What iOS actually permits — the official position

Facts in this section come from Apple documentation captured today. Each row names its capture.

3.1 A backgrounded app is suspended

"When another app is launched or the user returns to the Home screen, the foreground app moves to the background, following this sequence: … UIKit calls the app delegate's applicationDidEnterBackground(_:) method. The app's snapshot is taken. The app may be suspended again."

About the background execution sequence, https://developer.apple.com/documentation/uikit/about-the-background-execution-sequence (capture proof/02_store/captures/background_execution.txt, HTTP 200, retrieved 2026-08-04)

A suspended app runs no Dart. Cadence's in-app engine — the primary alarm by the code's own design statement (lib/alarm_backstop.dart:1-6) — stops ticking. This is the structural reason the backstop exists on Android, and the reason its absence on iOS is fatal rather than cosmetic.

3.2 .playback survives the silent switch and the lock screen, but not backgrounding, unless you declare audio

"When using this category, your app audio continues with the Silent switch set to silent or when the screen locks. … To continue playing audio when your app transitions to the background (for example, when the screen locks), add the audio value to the UIBackgroundModes key in your information property list file."

AVAudioSession.Category.playback, https://developer.apple.com/documentation/avfaudio/avaudiosession/category-swift.struct/playback (capture avaudiosession_playback.txt, HTTP 200, retrieved 2026-08-04)

Cadence sets .playback in two places — ios/Runner/AppDelegate.swift:145 for text-to-speech, and lib/audio/audio.dart:31-34 for ringtones:

    iOS: AudioContextIOS(
      category: AVAudioSessionCategory.playback,
      options: const {AVAudioSessionOptions.mixWithOthers},
    ),

But ios/Runner/Info.plist contains no UIBackgroundModes key. The whole file is 70 lines; the keys present are CADisableMinimumFrameDurationOnPhone, CFBundleDevelopmentRegion, CFBundleDisplayName, CFBundleExecutable, CFBundleIdentifier, CFBundleInfoDictionaryVersion, CFBundleName, CFBundlePackageType, CFBundleShortVersionString, CFBundleSignature, CFBundleVersion, LSRequiresIPhoneOS, UIApplicationSceneManifest, UIApplicationSupportsIndirectInputEvents, UILaunchStoryboardName, UIMainStoryboardFile, UISupportedInterfaceOrientations, UISupportedInterfaceOrientations~ipad. Verified by reading the file end-to-end.

audio is one of the accepted UIBackgroundModes values (UIBackgroundModes, https://developer.apple.com/documentation/bundleresources/information-property-list/uibackgroundmodes, capture uibackgroundmodes.txt, HTTP 200, retrieved 2026-08-04; the list is audio, bluetooth-central, bluetooth-peripheral, external-accessory, fetch, location, nearby-interaction, network-authentication, newsstand-content, processing, push-to-talk, remote-notification, voip).

Do not treat UIBackgroundModes: audio as the fix. It keeps the app alive while it is playing audio. A timer app that plays an inaudible keep-alive stream for 12 minutes so it can make a noise at the end is using a background service for something other than its declared purpose, which is what Guideline 2.5.4 addresses: "Multitasking apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc." (capture proof/03_market/captures/policy_apple_review_guidelines.txt:196). It also drains an iPad and is a well-known rejection vector. The supported mechanism for this exact job is the next section.

3.3 Local notifications are Apple's supported timer mechanism

"Create a UNTimeIntervalNotificationTrigger object when you want to schedule the delivery of a local notification after the number of seconds you specify elapses. You use this type of trigger to implement timers."

UNTimeIntervalNotificationTrigger, https://developer.apple.com/documentation/usernotifications/untimeintervalnotificationtrigger (capture un_time_interval_trigger.txt, HTTP 200, retrieved 2026-08-04)

A scheduled local notification is held by the system, not the app. It fires when the app is backgrounded, when the device is locked, and after the app is force-quit. This is the exact Android-AlarmManager-equivalent Cadence needs, and flutter_local_notifications already exposes it — the app simply never reaches it on iOS (§2).

3.4 What suppresses a local notification's sound

Suppressor Effect on a normal local notification Escape hatch Source
Silent / mute switch, mute mode Sound suppressed UNNotificationInterruptionLevel.critical — "bypasses the mute switch to play a sound" UNNotificationInterruptionLevel, capture un_interruption_level.txt
Focus / Do Not Disturb Notification withheld .timeSensitive — "breaks through system notification controls"; or .critical same capture
User denied notification permission Nothing delivered none — must be re-granted in Settings Asking permission to use notifications, capture un_request_authorization.txt
Notification longer than 30 s of audio System substitutes the default sound keep the tone under 30 s UNNotificationSound, capture un_notification_sound.txt

Verbatim, the four interruption levels:

case active — The system presents the notification immediately, lights up the screen, and can play a sound. case critical — The system presents the notification immediately, lights up the screen, and bypasses the mute switch to play a sound. case passive — The system adds the notification to the notification list without lighting up the screen or playing a sound. case timeSensitive — The system presents the notification immediately, lights up the screen, can play a sound, and breaks through system notification controls.

— https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel (capture un_interruption_level.txt, HTTP 200, retrieved 2026-08-04)

.timeSensitive is available to any app with the Time Sensitive Notifications capability and needs no Apple approval. .critical requires the entitlement in §6.

3.5 Where a custom alarm sound must live — and where Cadence's sounds actually are

"Audio files must already be on the user's device before the system can play them. … the UNNotificationSound object looks only in the following locations: The /Library/Sounds directory of the app's container directory. The /Library/Sounds directory of one of the app's shared group container directories. The main bundle of the current executable."

"The system sound facility plays custom alert sounds, so they must be in one of the following audio data formats: Linear PCM, MA4 (IMA/ADPCM), µLaw, aLaw. You can package the audio data in an aiff, wav, or caf file. Sound files must be less than 30 seconds in length. If the sound file is longer than 30 seconds, the system plays the default sound instead."

UNNotificationSound, https://developer.apple.com/documentation/usernotifications/unnotificationsound (capture un_notification_sound.txt, HTTP 200, retrieved 2026-08-04)

Cadence's 15 tone files live in assets/audio/ and are declared as Flutter assets (pubspec.yaml:44-46). On iOS, Flutter assets are packaged inside Runner.app/Frameworks/App.framework/flutter_assets/, which is not the main bundle root and not Library/Sounds. UNNotificationSound(named:) will not find them there.

The files themselves are otherwise compliant — measured with afinfo today:

File Format Duration
assets/audio/bell.wav 1 ch, 44100 Hz, Int16 (Linear PCM) 1.10 s
assets/audio/cascade.wav 1 ch, 44100 Hz, Int16 4.16 s (longest of the 15)
assets/audio/beep.wav 1 ch, 44100 Hz, Int16 0.35 s (shortest)
android/app/src/main/res/raw/cadence_alarm.wav — the Android backstop tone 1 ch, 44100 Hz, Int16 1.62 s

All are Linear PCM in a wav container and all are far under 30 s. The fix is packaging, not re-encoding: one WAV must additionally be copied to the Runner target's bundle root as a resource so UNNotificationSound(named: "cadence_alarm.wav") resolves.

One consequence a kitchen will feel: a local notification plays its sound once. Android's in-app engine repeats a ringtone with a floor of a 2 s gap (lib/audio/audio.dart:14-18 documents the 4 s tone / 2 s gap case). iOS gives one shot per scheduled notification. Ringing until acknowledged requires scheduling a short chain of notifications, or AlarmKit (§7).


4. The Android/iOS alarm-parity gap, side by side

Android has a complete second alarm system. iOS has none of it. Every Android row below is verified in the repo; every iOS row is verified as absent.

Capability Android — evidence iOS — evidence Gap
OS-held scheduled alarm AndroidScheduleMode.alarmClock (= AlarmManager.setAlarmClock, the Doze-immune tier), lib/alarm_backstop.dart:192-194 Never scheduled — init() throws, _ready == false, sync() returns at :123 Total
Degrade path when exact alarms denied inexactAllowWhileIdle + critical banner, :209-217 n/a — nothing to degrade from Total
Alarm-stream audio routing AudioAttributesUsage.alarm, :56; AndroidUsageType.alarm in lib/audio/audio.dart:28 .playback only — the closest analogue, documented as such in AppDelegate.swift:18-20. No per-stream alarm volume on iOS; getAlarmVolume deliberately returns nil (AppDelegate.swift:54-57) Partial by platform design
Full-screen alarm UI when locked fullScreenIntent: true + AndroidNotificationCategory.alarm, :52-53; USE_FULL_SCREEN_INTENT permission, AndroidManifest.xml:16 No equivalent short of AlarmKit (§7) Total
Custom alarm tone from the OS notification RawResourceAndroidNotificationSound('cadence_alarm')res/raw/cadence_alarm.wav, :55 No DarwinNotificationDetails, :61-62; and the WAVs are not in the bundle root (§3.5) Total
Survives device reboot RECEIVE_BOOT_COMPLETED (AndroidManifest.xml:17) + a boot receiver for BOOT_COMPLETED, MY_PACKAGE_REPLACED and QUICKBOOT_POWERON (:46-56) Scheduled UNNotificationRequests do survive reboot once scheduled — but none are ever scheduled Total, by consequence
Notification permission requested requestNotificationsPermission(), :79; POST_NOTIFICATIONS, AndroidManifest.xml:12 Never requested. UNUserNotificationCenter.requestAuthorization appears nowhere in ios/ or lib/ Total
Exact-alarm permission requested requestExactAlarmsPermission(), :80; USE_EXACT_ALARM + SCHEDULE_EXACT_ALARM, AndroidManifest.xml:13-15 No iOS analogue needed n/a
Screen kept awake WakelockPlus.enable(), lib/main.dart:28; WAKE_LOCK, AndroidManifest.xml:3 wakelock_plus 1.7.0 does implement this on iOS through UIApplication.isIdleTimerDisabled (…/wakelock_plus-1.7.0/ios/wakelock_plus/Sources/wakelock_plus/WakelockPlusPlugin.m:30,38), so the mechanism exists — but RUNTIME-UNVERIFIED, since the target has never compiled Low risk
Haptics on alarm enableVibration: true, :57; VIBRATE, AndroidManifest.xml:2 vibration plugin is cross-platform; the notification-side vibration has no iOS equivalent Partial
Entitlements file n/a No .entitlements file exists anywhere under ios/ (find ios -name "*.entitlements" → empty), and project.pbxproj contains no CODE_SIGN_ENTITLEMENTS setting (grep → empty) Blocks Critical Alerts and Time Sensitive

5. Configuration B — the minimum repair, specified

This is the smallest change that gives iOS a real backstop. It is compliance-and-defect plumbing, not a new user-facing feature, so it is in scope under AGENT_RULES.md R6.

B1 — give the plugin Darwin initialisation settings. Replace lib/alarm_backstop.dart:72-76:

      await _plugin.initialize(
        settings: const InitializationSettings(
          android: AndroidInitializationSettings('@mipmap/ic_launcher'),
          iOS: DarwinInitializationSettings(
            requestAlertPermission: true,
            requestSoundPermission: true,
            requestBadgePermission: false,
          ),
        ),
      );

All three parameter names are real on this plugin version, and requestBadgePermission defaults to true, so setting it false is a deliberate narrowing — a kitchen timer has no business badging an icon (…/flutter_local_notifications-22.1.0/lib/src/platform_specifics/darwin/initialization_settings.dart:8-10). The same class exposes requestCriticalPermission, default false (:12, :51), whose own doc comment reads "Subject to specific approval from Apple" and links the request form. Leave it false until the entitlement in §6 is actually granted.

B2 — give the notification Darwin details. Replace lib/alarm_backstop.dart:61-62:

  static const DarwinNotificationDetails _darwin = DarwinNotificationDetails(
    presentAlert: true,   // iOS 10-14 only
    presentBanner: true,  // iOS 15+
    presentList: true,    // iOS 15+
    presentSound: true,
    presentBadge: false,
    sound: 'cadence_alarm.wav',
    interruptionLevel: InterruptionLevel.timeSensitive,
  );

  static const NotificationDetails _details =
      NotificationDetails(android: _channel, iOS: _darwin);

All seven parameters exist on this plugin version's constructor (…/darwin/notification_details.dart:8-22), including interruptionLevel (:20, :142) and criticalSoundVolume (:144-148) for later. presentAlert is documented in that same file as applicable only to iOS 10-14, with presentBanner and presentList replacing it from iOS 15 — which is why all three are set, given the app's IPHONEOS_DEPLOYMENT_TARGET = 13.0.

B3 — put the sound where iOS can find it. Copy android/app/src/main/res/raw/cadence_alarm.wav to ios/Runner/cadence_alarm.wav and add it to the Runner target's Copy Bundle Resources phase, so it lands at Runner.app/cadence_alarm.wav (§3.5). It is 1.62 s of 44.1 kHz Int16 Linear PCM — compliant as-is.

B4 — add the Time Sensitive Notifications capability. .timeSensitive requires the com.apple.developer.usernotifications.time-sensitive entitlement, which needs a Runner.entitlements file (none exists) and CODE_SIGN_ENTITLEMENTS in project.pbxproj (absent). Unlike Critical Alerts this is self-service in Xcode, with no Apple approval step.

B5 — the platform guard the code does not have. lib/ contains exactly two Platform.is* checks, both in lib/journal.dart (:117 Android, :123 iOS), with a Platform.operatingSystem fallback at :129 — all three only to build the device-description string for the journal header. alarm_backstop.dart has none. After B1–B2 it does not need one — the plugin dispatches on defaultTargetPlatform internally — but the Diag.fail('backstop-init', …, isCritical: true) path at :95 must stay, so a genuine iOS permission refusal still raises the operator banner rather than being mistaken for the old unconditional throw.

How to prove the repair. Two tests, and the first one is already written:

  1. Unit, red now / green after — the probe in §2.7 (proof/02_store/s02_probe_platform_test.dart) is exactly this test. Fold its PROBE-3 arm into test/backstop_test.dart with the assertion inverted to expect(b.ready, isTrue) and expect(calls, isNotEmpty). It is red at 03a176e — proven, IOS_BACKSTOP_READY=false CALLS=[] — and green after B1–B2. Extend the same arm to assert that a sync() on a running timer produces a zonedSchedule call carrying a non-null iOS payload, which covers B2 and B3.
  2. The verdict-flipping device test — carried forward verbatim from the 2026-07-19 Codex audit via research/01_prior_work.md §2.3: on the target iPad, install the build, start a 60-second timer, lock the device, and prove from an external clock or video that the audible alarm begins at 60 seconds without reopening the app. Then repeat with the app force-quit, with the silent switch on, and with a Focus enabled. Until that run exists, iOS alarm behaviour is RUNTIME-UNVERIFIED regardless of what the code says. The same gap is open on Android: commit f46d142 states verbatim that the OS backstop never actually rang in the only field log — «le secours OS n'a jamais reellement sonne dans ce log».

6. Critical Alerts — what it is, how it is requested, and what Apple says about approval

The entitlement.

Key: com.apple.developer.usernotifications.critical-alerts Type: Boolean "If your app has this entitlement, then it can request criticalAlert authorization to receive push notifications that cause the system to play a sound even when the app is locked, muted, or a person uses Do Not Disturb focus. Your app can specify a custom sound and volume for critical alerts. To request this entitlement for your app, fill out the request form."

Critical Alerts, https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.critical-alerts (capture entitlement_critical_alerts.txt, HTTP 200, retrieved 2026-08-04)

How it is requested. The form at https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/ redirects to Apple's sign-in wall for an unauthenticated client. Our capture of that URL returned HTTP 200 with title Sign In - Apple and 89 bytes of body text after three attempts, with a final url of https://idmsa.apple.com/IDMSWebAuth/signin.html?path=%2Fcontact%2Frequest%2Fnotifications-critical-alerts-entitlement%2F&… (proof/02_store/captures/request_critical_alerts.txt; MANIFEST_apple.md row request_critical_alerts, tries=3, retrieved 2026-08-04T10:51:30Z).

What Apple's stated approval criteria are. Per R1 and R3, the honest answer: Apple publishes no approval criteria on any page reachable without an Apple Developer sign-in. The entitlement documentation states only that you "fill out the request form"; it names no eligibility test, no category list, and no service-level commitment. The App Review Guidelines capture (94,514 bytes, retrieved 2026-08-04) contains no criteria for this entitlement either.

The exact missing artifact and the test that would settle it: the content of https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/ retrieved while signed in to the project owner's or Serge's Apple Developer account. That page states the questions Apple asks and any stated eligibility. Retrieving it requires an enrolled account, which does not yet exist (checklist item A1 in 02_apple_app_store.md). Do not substitute a blog post or a forum thread for it — R11.

The practical read for this product. Critical Alerts is a request, not a switch, and its outcome is not knowable in advance from published material. Do not put the product's core promise on a dependency whose approval criteria Apple does not publish. The alternative in §7 has no approval step at all.


7. AlarmKit — the mechanism that actually matches the product, and its price

Apple shipped a framework for exactly this problem.

AlarmKit — "Schedule prominent alarms and countdowns to help people manage their time." iOS 26.0+ · iPadOS 26.0+ · Mac Catalyst 26.0+ "Use AlarmKit to create custom alarms and timers in your app. … It supports one-time and repeating alarms, with the option for countdown durations and snooze functionality. AlarmKit handles alarm authorization and provides UI for both templated and widget presentations. It supports traditional alarms, timers, or both, and provides methods to schedule, pause, resume, and cancel alarms."

— https://developer.apple.com/documentation/alarmkit (capture alarmkit.txt, HTTP 200, retrieved 2026-08-04)

The decisive sentence is in the sample-code article:

"An alarm is an alert that presents at a pre-determined time based on a schedule or after a countdown. It overrides both a device's focus and silent mode, if necessary."

Scheduling an alarm with AlarmKit, https://developer.apple.com/documentation/alarmkit/scheduling-an-alarm-with-alarmkit (capture alarmkit_scheduling.txt, HTTP 200, retrieved 2026-08-04)

That is Configuration C without an entitlement request. What it costs, from the same capture:

Requirement Verbatim source Cost to Cadence
iOS/iPadOS 26.0 minimum "iOS 26.0+ / iPadOS 26.0+ / Mac Catalyst 26.0+" IPHONEOS_DEPLOYMENT_TARGET = 13.0 today (ios/Runner.xcodeproj/project.pbxproj:363). Either raise the floor, or gate AlarmKit behind an availability check and keep §5 as the fallback for older iPads.
Xcode 26.0+ "Xcode 26.0+" Not installed at all today.
NSAlarmKitUsageDescription in Info.plist "If the NSAlarmKitUsageDescription key is missing or its value is an empty string, apps can't schedule alarms with AlarmKit." One Info.plist key. Absent today.
Authorization "This sample prompts people to authorize the app … by calling requestAuthorization() on AlarmManager. … If this sample doesn't get this authorization, then any alarm created by the app isn't scheduled and subsequently doesn't alert." New Swift code in AppDelegate.swift plus a method channel, or a Flutter plugin. None exists.
A widget extension, if countdown UI is used "AlarmKit expects a widget extension if an app supports a countdown presentation. Otherwise, the system may unexpectedly dismiss alarms and fail to alert." A second target in the Xcode project. Cadence has one target plus RunnerTests.
Swift-only framework The API surface is AlarmManager, Alarm, AlarmPresentation, AlarmAttributes No Flutter plugin in Cadence's dependency set touches AlarmKit. This is native work behind a method channel — which is exactly the shape AppDelegate.swift:5-10 already establishes for cadence/volume and cadence/tts.

RUNTIME-UNVERIFIED, and the artifact that would settle it: whether AlarmKit's system-owned alarm UI is acceptable in a kitchen kiosk where the operator wants Cadence's own board on screen. Settling it requires an iPadOS 26 device build. Nothing in the documentation forbids it; the sample explicitly supports "Timers which alert after a countdown, and start immediately".


8. Second-order iOS risks found while reading, each with its evidence

8.1 — .mixWithOthers may be the wrong option for an alarm. Both audio sessions set mixWithOthers (AppDelegate.swift:145, lib/audio/audio.dart:33). Apple: "By default, using this category implies that your app's audio is nonmixable—activating your session will interrupt any other audio sessions which are also nonmixable. To allow mixing for this category, use the mixWithOthers option." (capture avaudiosession_playback.txt.) Cadence has therefore opted out of interrupting other audio. If the kitchen iPad is also playing a radio stream or the staff's music, Cadence's alarm will mix under it instead of ducking or interrupting it. On Android the equivalent choice is explicit and different: AndroidAudioFocus.none with AndroidUsageType.alarm (lib/audio/audio.dart:28-29), which on Android still routes to the alarm stream. This is a deliberate iOS choice with a real kitchen consequence, and it is RUNTIME-UNVERIFIED. The test: play music on the iPad, fire a timer, record whether the alarm is audible over it.

8.2 — the iOS platform adapter has never been compiled, and it uses a modern Flutter API surface. AppDelegate.swift:22 declares FlutterImplicitEngineDelegate and didInitializeImplicitFlutterEngine(_:), and ios/Runner/SceneDelegate.swift exists alongside a UIApplicationSceneManifest in Info.plist:29-49. This is the current Flutter 3.44 template shape, so it is plausible; but "plausible" is not "compiles". The artifact that would settle it: flutter build ios --release on a Mac with Xcode 26. Until then the two method channels cadence/volume and cadence/tts are unproven on iOS, which means voice announcements — lib/audio/voice.dart — are unproven too.

8.3 — no CocoaPods, and the project is on Swift Package Manager. There is no ios/Podfile and no ios/Podfile.lock; instead ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/ is present. Nine plugins resolve for iOS (.flutter-plugins-dependencies): audioplayers_darwin, device_info_plus, flutter_local_notifications, package_info_plus, path_provider_foundation, share_plus, shared_preferences_foundation, vibration, wakelock_plus. The absence of CocoaPods on this machine is therefore not necessarily a blocker for the build — but it is one more thing that has never been executed.

8.4 — the launch screen is a 1×1 pixel placeholder. All three ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage*.png files measure 1×1 pixels (sips -g pixelWidth -g pixelHeight), while ios/Runner/Base.lproj/LaunchScreen.storyboard declares <image name="LaunchImage" width="168" height="185"/>. The app will launch to a plain white screen. Not a rejection on its own; it is the first thing a reviewer sees.

8.5 — the iOS app icon is the stock Flutter logo. Covered in detail in 02_apple_app_store.md §8; recorded here because it is the most visible iOS-side artifact of flutter_launcher_icons: ios: false (pubspec.yaml:35).


9. Coverage manifest — every iOS-relevant file in scope, and what was checked

File Lines What was checked
ios/Runner/Info.plist 70 Read end-to-end. Enumerated all 18 keys. Confirmed absent: UIBackgroundModes, ITSAppUsesNonExemptEncryption, ITSEncryptionExportComplianceCode, NSAlarmKitUsageDescription, any NS*UsageDescription. Confirmed present: CFBundleDisplayName = Cadence (:9-10), orientations for iPhone (:56-61) and iPad (:62-68), scene manifest (:29-49).
ios/Runner/AppDelegate.swift 207 Read end-to-end. FlutterImplicitEngineDelegate shape (:22,37), the "NOT YET COMPILED" header (:12-13), cadence/volume returning nil by design (:54-59), cadence/tts handler (:66-136), .playback/.spokenAudio/.mixWithOthers session (:145), speak/complete lifecycle (:157-192). No UNUserNotificationCenter, no requestAuthorization, no AlarmKit, no UIBackgroundModes-dependent code.
ios/Runner/SceneDelegate.swift Presence confirmed; paired with the UIApplicationSceneManifest in Info.plist.
ios/Runner.xcodeproj/project.pbxproj Grepped for every signing and identity setting. PRODUCT_BUNDLE_IDENTIFIER = dev.sergemio.cadence (:385) and …RunnerTests (:401); IPHONEOS_DEPLOYMENT_TARGET = 13.0 (:363); TARGETED_DEVICE_FAMILY = "1,2" (:367); "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer" (:349); CODE_SIGN_STYLE = Automatic only on the RunnerTests target (:397). Confirmed absent: DEVELOPMENT_TEAM, PROVISIONING_PROFILE_SPECIFIER, CODE_SIGN_ENTITLEMENTS, any .xcprivacy file reference.
ios/Runner/Assets.xcassets/AppIcon.appiconset/ 15 PNGs Every file measured with sips (dimensions + alpha). Icon-App-1024x1024@1x.png read as an image: it is the stock Flutter logo. hasAlpha: no on all sampled files.
ios/Runner/Assets.xcassets/LaunchImage.imageset/ 3 PNGs All three measured 1×1 px.
ios/Runner/Base.lproj/LaunchScreen.storyboard Read; declares LaunchImage at 168×185 against 1×1 assets.
ios/ tree, whole 48 files find ios -type f enumerated. Confirmed absent: any *.entitlements, any *.xcprivacy, Podfile, Podfile.lock. Confirmed present: Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/ (Swift Package Manager path).
lib/alarm_backstop.dart 279 Read end-to-end. Init path (:69-97), _desired (:100-116), sync (:122-149), _flushSchedules (:154-168), _schedule (:177-221), _cancel (:223-233), onBackground (:238-248), showNow (:252-264), onForeground (:268-278). Every _ready guard located and tabulated.
lib/ui/home.dart 722 Grepped for every backstop call site: :38 construction, :149 init, :152/:190/:256 sync, :189 onForeground, :199 onBackground, :300 showNow, :685-686 banner mapping.
lib/audio/audio.dart Read _alarmCtx (:23-35) for the iOS AudioContextIOS category and options; _play (:58-78) for the asset source path; init (:44-54).
lib/main.dart 58 Read end-to-end. WakelockPlus.enable() (:28-32), immersiveSticky (:33), kAppVersion (:20).
lib/journal.dart Read the iOS-relevant parts: dart:io import (:16), Platform.isIOS device string (:123-127), file ops (:69-72, :170-171, :224-229).
lib/i18n.dart Located backstopDown in both languages (:79 FR, :124 EN).
pubspec.yaml 68 Read end-to-end. All 10 direct dependencies, flutter_launcher_icons: ios: false (:35), asset declarations (:44-46).
.flutter-plugins-dependencies Parsed; enumerated the nine iOS plugins.
android/app/src/main/AndroidManifest.xml Grepped for the permission and receiver set, to build the parity table in §4.
assets/audio/*.wav + android/.../res/raw/cadence_alarm.wav 16 files Every file measured with afinfo for data format and duration, against Apple's UNNotificationSound format and 30-second constraints.
~/.pub-cache/.../flutter_local_notifications-22.1.0/ Read initialize (:111-175) and zonedSchedule (:409-459) in flutter_local_notifications_plugin.dart; DarwinInitializationSettings (initialization_settings.dart:12,53); DarwinNotificationDetails (notification_details.dart:14,20,99,142,144-148).
test/backstop_test.dart Read the mock-channel setup (:33, :41-67) so the §2.7 probe could reproduce it faithfully; confirmed the suite installs AndroidFlutterLocalNotificationsPlugin.registerWith() and never an iOS override.
proof/02_store/s02_probe_platform_test.dart 68 lines Written by this stream and executed on the copy at a scratch working copy. Four probes: default test platform, plugin initialize under iOS, Backstop.ready under iOS, Backstop.ready under Android as control.
proof/02_store/probe_ios_backstop.txt The recorded run, stamped through proof/run_and_record.sh [not published] with command, cwd, UTC time, Flutter version, git sha, tree state and exit code (R12).