store-preflight-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ *.sarif
10
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asif
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: store-preflight-mcp
3
+ Version: 0.1.0
4
+ Summary: App Store / Play rejection preflight for agents โ€” maps API usage to the declaration it obliges, before the binary is rejected
5
+ Project-URL: Homepage, https://github.com/asif786ka/store-preflight-mcp
6
+ Project-URL: Repository, https://github.com/asif786ka/store-preflight-mcp
7
+ Project-URL: Issues, https://github.com/asif786ka/store-preflight-mcp/issues
8
+ Author: Asif
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: android,app-store,compliance,flutter,google-play,ios,mcp,privacy-manifest,react-native
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: mcp>=1.2.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # store-preflight-mcp
25
+
26
+ **Your agent just wrote code that will get your app rejected. It has no idea.**
27
+
28
+ An MCP server that maps *API usage โ†’ required declaration โ†’ dated policy*, so a
29
+ coding agent finds out at write time instead of from an App Store Connect email
30
+ three days after the PR merged.
31
+
32
+ ```
33
+ $ store-preflight scan . --date 2026-07-15
34
+
35
+ ๐Ÿ›‘ UserDefaults requires a declared reason in PrivacyInfo.xcprivacy
36
+ ITMS-91053
37
+ No PrivacyInfo.xcprivacy exists anywhere in this project. Code touches
38
+ UserDefaults APIs, so App Store Connect will refuse the upload.
39
+ โ†’ ios/RejectedApp/AnalyticsModule.swift:7 private let defaults = UserDefaults.standard
40
+ fix: PrivacyInfo.xcprivacy โ†’ NSPrivacyAccessedAPICategoryUserDefaults
41
+
42
+ โœ— failing: 7 finding(s) at or above blocker
43
+ ```
44
+
45
+ That's the whole product. An agent adds an analytics module, touches
46
+ `UserDefaults`, and nothing in the world tells it that Apple has required a
47
+ `CA92.1` entry in a file it has never heard of since **1 May 2024**. The binary
48
+ is rejected days later, by which point nobody remembers which PR did it.
49
+
50
+ Doc MCPs describe APIs. None of them map an API to the declaration it obliges, or
51
+ to the date that obligation acquired teeth.
52
+
53
+ ---
54
+
55
+ ## Why dates are the whole design
56
+
57
+ This is the sibling of [mobile-docs-mcp](https://github.com/asif786ka/mobile-docs-mcp),
58
+ and it reuses its core insight. There, a symbol isn't "real" โ€” it's real *within a
59
+ version range* (`since` / `deprecated` / `removed`). Here, a rule isn't "true" โ€”
60
+ it's true *within a date window*:
61
+
62
+ ```python
63
+ PolicyWindow(
64
+ announced = "2023-06-05", # WWDC23
65
+ warning_from = "2024-03-13", # email only, upload still succeeds
66
+ enforced_from = "2024-05-01", # hard block
67
+ )
68
+ ```
69
+
70
+ `PolicyWindow.status_on(date)` is the direct analogue of
71
+ `VersionRange.status_at(version)`. Same model, different axis โ€” because store
72
+ policy ships on deadlines, not releases.
73
+
74
+ This isn't decoration. It's what makes the tool usable:
75
+
76
+ - **Severity follows the calendar.** A blocker that doesn't bite until October is
77
+ reported as a *warning* in July, automatically. Nobody hand-edits severities as
78
+ deadlines pass.
79
+ - **You can scan the future.** `--date 2026-10-28` tells you what breaks when a
80
+ deadline lands, before it lands.
81
+ - **The tests don't rot.** Every assertion is pinned to a fixed date, so the suite
82
+ doesn't silently change verdict when a deadline passes.
83
+
84
+ The same unchanged repo, scanned on two dates:
85
+
86
+ ```
87
+ $ store-preflight scan . --date 2026-07-15
88
+ โ„น๏ธ Geofencing is no longer an approved use case for a location foreground service
89
+ โš ๏ธ READ_CONTACTS requires a declaration for apps targeting Android 17+
90
+
91
+ $ store-preflight scan . --date 2026-10-28
92
+ โš ๏ธ Geofencing is no longer an approved use case for a location foreground service
93
+ ๐Ÿ›‘ READ_CONTACTS requires a declaration for apps targeting Android 17+
94
+ ```
95
+
96
+ Not one byte of that repo changed. **That failure mode is invisible to every
97
+ other tool in the toolchain** โ€” a linter diffs your code, and your code is fine.
98
+
99
+ (Why does `READ_CONTACTS` escalate to a blocker while geofencing stops at a
100
+ warning? Because the permission string is either in your manifest or it isn't,
101
+ whereas whether *geofencing* is what justifies your location FGS depends on a
102
+ form you filed in Play Console, which isn't in the repo. See
103
+ [On being trustworthy](#on-being-trustworthy).)
104
+
105
+ ---
106
+
107
+ ## Install
108
+
109
+ ```bash
110
+ uvx store-preflight-mcp # MCP server (stdio)
111
+ pipx install store-preflight-mcp # CLI too
112
+ ```
113
+
114
+ Claude Code / any MCP client:
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "store-preflight": {
120
+ "command": "uvx",
121
+ "args": ["store-preflight-mcp"]
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ Zero non-`mcp` runtime dependencies. No crawl, no API key, no network. The
128
+ ruleset ships as Python and the whole scan is local.
129
+
130
+ ---
131
+
132
+ ## Tools
133
+
134
+ | Tool | Use |
135
+ |---|---|
136
+ | `preflight_scan` | Scan a repo. What will be rejected, where, and why. |
137
+ | `check_api_declaration` | **Before** writing: "I'm about to use `UserDefaults` โ€” what must I declare?" |
138
+ | `get_required_declaration` | The exact key + reason code + paste-ready snippet. |
139
+ | `explain_rejection` | Paste `ITMS-91053` from the email โ†’ a concrete fix. |
140
+ | `list_policy_deadlines` | The dated calendar. What's enforced, what lands next. |
141
+
142
+ `check_api_declaration` is the one that changes agent behaviour โ€” it's cheap
143
+ enough to call before writing the code, which is the only time the answer is free.
144
+
145
+ ```
146
+ > check_api_declaration("UserDefaults", store="apple")
147
+
148
+ ๐Ÿ›‘ BLOCKER ยท status enforced โ€” enforced since 2024-05-01 (806 days ago)
149
+
150
+ You must declare: NSPrivacyAccessedAPICategoryUserDefaults in PrivacyInfo.xcprivacy
151
+
152
+ Valid values:
153
+ ยท CA92.1 โ€” Read/write info accessible only to the app itself...
154
+ ยท 1C8F.1 โ€” ...only to apps, extensions and App Clips in the same App Group...
155
+ ยท C56D.1 โ€” Third-party SDK only: wrapper function around UserDefaults...
156
+ ยท AC6B.1 โ€” Read com.apple.configuration.managed (MDM managed app config)...
157
+ ```
158
+
159
+ ---
160
+
161
+ ## CI
162
+
163
+ ```yaml
164
+ - run: pipx install store-preflight-mcp
165
+ - run: store-preflight scan . --format sarif -o preflight.sarif
166
+ - uses: github/codeql-action/upload-sarif@v3
167
+ with: { sarif_file: preflight.sarif }
168
+ ```
169
+
170
+ SARIF means findings render as inline PR annotations, on the line that caused
171
+ them. `--fail-on blocker` (the default) exits non-zero, so a rejection becomes a
172
+ red check instead of an email.
173
+
174
+ ---
175
+
176
+ ## Coverage
177
+
178
+ **App Store**
179
+ - All **5** required-reason API categories, all **17** reason codes, each with
180
+ Apple's verbatim meaning โ€” `UserDefaults`, `FileTimestamp`, `SystemBootTime`,
181
+ `DiskSpace`, `ActiveKeyboards`
182
+ - **17** `Info.plist` purpose strings, incl. the iOS 17 calendar split
183
+ (`NSCalendarsFullAccessUsageDescription`) most projects still miss
184
+ - Privacy manifest validity: ITMS-91054 / 91055 / 91056
185
+ - All **86** privacy-impacting SDKs + npm/pub โ†’ framework aliasing
186
+ - `NSPrivacyTracking` without `NSPrivacyTrackingDomains`
187
+ - The Xcode 26 / iOS 26 SDK upload gate (since 2026-04-28)
188
+ - All **6** ITMS rejection codes decoded
189
+
190
+ **Google Play**
191
+ - Restricted permissions: `QUERY_ALL_PACKAGES`, `MANAGE_EXTERNAL_STORAGE`,
192
+ SMS/Call Log, `ACCESS_BACKGROUND_LOCATION`, `REQUEST_INSTALL_PACKAGES`,
193
+ `USE_EXACT_ALARM`, `USE_FULL_SCREEN_INTENT`, accessibility, photo/video
194
+ - **14** data safety categories with their data types
195
+ - All **14** foreground service types (including `mediaProcessing`, which most
196
+ lists miss), each with its required permission and runtime prerequisite
197
+ - Advertising ID, including the auto-merged-from-`play-services-ads` trap
198
+ - **New for 2026:** the Contacts policy (2026-10-28) and the geofencing
199
+ withdrawal (2026-08-26)
200
+
201
+ **Languages:** Swift, Objective-C, Kotlin, Java, JS/TS, Dart โ€” plus
202
+ `Info.plist`, `PrivacyInfo.xcprivacy`, `AndroidManifest.xml`, `package.json`,
203
+ `pubspec.yaml`, `Podfile`. React Native, Flutter and native all work; no build
204
+ required.
205
+
206
+ ---
207
+
208
+ ## On being trustworthy
209
+
210
+ A preflight tool that cries wolf gets `--ignore`d within a week, and then it may
211
+ as well not exist. Three deliberate concessions:
212
+
213
+ **Findings carry their own epistemics.** Apple's required-reason rules are
214
+ mechanically checkable: the symbol is in your binary or it isn't. Play's data
215
+ safety form is *not* โ€” it keys on **transmission off-device**, not on holding a
216
+ permission. Google says plainly that on-device processing needs no disclosure. So
217
+ `READ_CONTACTS` does **not** imply a Contacts disclosure, and every such mapping
218
+ is marked `heuristic`: it raises a question, and **a heuristic finding can never
219
+ fail your build** (enforced by a test).
220
+
221
+ **The target API level is not hardcoded.** When this ruleset was built
222
+ (2026-07-15), Google's own timeline page was internally inconsistent โ€” metadata
223
+ dated May 2026, content still describing the August 2025 / API 35 deadline โ€” and
224
+ *no API 36 announcement existed on any Google property*, despite third-party
225
+ blogs confidently asserting one. So the **rule** is encoded ("within one year of
226
+ the latest major Android release") along with `last_verified` and a
227
+ `needs-live-check` flag. A tool that states a fake deadline confidently is worse
228
+ than one that says "verify this".
229
+
230
+ **The SDK check is a prompt, not a verdict.** Apple's requirement attaches when
231
+ an app is new or an update *adds* a listed SDK โ€” not to every app that contains
232
+ Firebase. And "any SDKs that repackage those on the list" are included, so exact
233
+ matching under-detects. Both facts make a confident verdict impossible, so it
234
+ ships as a warning that says why.
235
+
236
+ What we do *not* concede: comments and string literals are stripped before symbol
237
+ matching (with line numbers preserved), `node_modules`/`Pods` are never walked,
238
+ and `tools:node="remove"` is honoured โ€” so the correct way to drop the
239
+ auto-merged `AD_ID` permission isn't reported as the violation.
240
+
241
+ ---
242
+
243
+ ## Verify it
244
+
245
+ ```bash
246
+ python smoke_test.py
247
+ ```
248
+
249
+ 70 assertions, no network. Four fixture repos: one that gets rejected, the same
250
+ app fixed, a manifest that is wrong on its own terms, and one whose code never
251
+ changes while the policy date moves under it. The suite also pins the
252
+ facts that are easy to get wrong:
253
+
254
+ ```
255
+ โœ“ exactly 5 required-reason categories
256
+ โœ“ exactly 17 reason codes
257
+ โœ“ 86 SDKs on the commonly-used list
258
+ โœ“ getattrlist maps to BOTH FileTimestamp and DiskSpace (not 1:1)
259
+ โœ“ ITMS-91054 title is 'Invalid API category declaration'
260
+ โœ“ 14 foreground service types incl. mediaProcessing
261
+ โœ“ target SDK is a rule, not a hardcoded literal
262
+ โœ“ no heuristic finding is ever a blocker
263
+ ```
264
+
265
+ Those aren't padding โ€” each one is a mistake that was actually made and caught
266
+ during the build. `getattrlist` really does oblige two separate declarations. The
267
+ SDK list really is 86, not 87, because Apple publishes "BoringSSL / openssl_grpc"
268
+ as one entry.
269
+
270
+ See [RULES.md](RULES.md) for provenance and how to re-verify against Apple's
271
+ JSON feed.
272
+
273
+ ## Licence
274
+
275
+ MIT
@@ -0,0 +1,252 @@
1
+ # store-preflight-mcp
2
+
3
+ **Your agent just wrote code that will get your app rejected. It has no idea.**
4
+
5
+ An MCP server that maps *API usage โ†’ required declaration โ†’ dated policy*, so a
6
+ coding agent finds out at write time instead of from an App Store Connect email
7
+ three days after the PR merged.
8
+
9
+ ```
10
+ $ store-preflight scan . --date 2026-07-15
11
+
12
+ ๐Ÿ›‘ UserDefaults requires a declared reason in PrivacyInfo.xcprivacy
13
+ ITMS-91053
14
+ No PrivacyInfo.xcprivacy exists anywhere in this project. Code touches
15
+ UserDefaults APIs, so App Store Connect will refuse the upload.
16
+ โ†’ ios/RejectedApp/AnalyticsModule.swift:7 private let defaults = UserDefaults.standard
17
+ fix: PrivacyInfo.xcprivacy โ†’ NSPrivacyAccessedAPICategoryUserDefaults
18
+
19
+ โœ— failing: 7 finding(s) at or above blocker
20
+ ```
21
+
22
+ That's the whole product. An agent adds an analytics module, touches
23
+ `UserDefaults`, and nothing in the world tells it that Apple has required a
24
+ `CA92.1` entry in a file it has never heard of since **1 May 2024**. The binary
25
+ is rejected days later, by which point nobody remembers which PR did it.
26
+
27
+ Doc MCPs describe APIs. None of them map an API to the declaration it obliges, or
28
+ to the date that obligation acquired teeth.
29
+
30
+ ---
31
+
32
+ ## Why dates are the whole design
33
+
34
+ This is the sibling of [mobile-docs-mcp](https://github.com/asif786ka/mobile-docs-mcp),
35
+ and it reuses its core insight. There, a symbol isn't "real" โ€” it's real *within a
36
+ version range* (`since` / `deprecated` / `removed`). Here, a rule isn't "true" โ€”
37
+ it's true *within a date window*:
38
+
39
+ ```python
40
+ PolicyWindow(
41
+ announced = "2023-06-05", # WWDC23
42
+ warning_from = "2024-03-13", # email only, upload still succeeds
43
+ enforced_from = "2024-05-01", # hard block
44
+ )
45
+ ```
46
+
47
+ `PolicyWindow.status_on(date)` is the direct analogue of
48
+ `VersionRange.status_at(version)`. Same model, different axis โ€” because store
49
+ policy ships on deadlines, not releases.
50
+
51
+ This isn't decoration. It's what makes the tool usable:
52
+
53
+ - **Severity follows the calendar.** A blocker that doesn't bite until October is
54
+ reported as a *warning* in July, automatically. Nobody hand-edits severities as
55
+ deadlines pass.
56
+ - **You can scan the future.** `--date 2026-10-28` tells you what breaks when a
57
+ deadline lands, before it lands.
58
+ - **The tests don't rot.** Every assertion is pinned to a fixed date, so the suite
59
+ doesn't silently change verdict when a deadline passes.
60
+
61
+ The same unchanged repo, scanned on two dates:
62
+
63
+ ```
64
+ $ store-preflight scan . --date 2026-07-15
65
+ โ„น๏ธ Geofencing is no longer an approved use case for a location foreground service
66
+ โš ๏ธ READ_CONTACTS requires a declaration for apps targeting Android 17+
67
+
68
+ $ store-preflight scan . --date 2026-10-28
69
+ โš ๏ธ Geofencing is no longer an approved use case for a location foreground service
70
+ ๐Ÿ›‘ READ_CONTACTS requires a declaration for apps targeting Android 17+
71
+ ```
72
+
73
+ Not one byte of that repo changed. **That failure mode is invisible to every
74
+ other tool in the toolchain** โ€” a linter diffs your code, and your code is fine.
75
+
76
+ (Why does `READ_CONTACTS` escalate to a blocker while geofencing stops at a
77
+ warning? Because the permission string is either in your manifest or it isn't,
78
+ whereas whether *geofencing* is what justifies your location FGS depends on a
79
+ form you filed in Play Console, which isn't in the repo. See
80
+ [On being trustworthy](#on-being-trustworthy).)
81
+
82
+ ---
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ uvx store-preflight-mcp # MCP server (stdio)
88
+ pipx install store-preflight-mcp # CLI too
89
+ ```
90
+
91
+ Claude Code / any MCP client:
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "store-preflight": {
97
+ "command": "uvx",
98
+ "args": ["store-preflight-mcp"]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Zero non-`mcp` runtime dependencies. No crawl, no API key, no network. The
105
+ ruleset ships as Python and the whole scan is local.
106
+
107
+ ---
108
+
109
+ ## Tools
110
+
111
+ | Tool | Use |
112
+ |---|---|
113
+ | `preflight_scan` | Scan a repo. What will be rejected, where, and why. |
114
+ | `check_api_declaration` | **Before** writing: "I'm about to use `UserDefaults` โ€” what must I declare?" |
115
+ | `get_required_declaration` | The exact key + reason code + paste-ready snippet. |
116
+ | `explain_rejection` | Paste `ITMS-91053` from the email โ†’ a concrete fix. |
117
+ | `list_policy_deadlines` | The dated calendar. What's enforced, what lands next. |
118
+
119
+ `check_api_declaration` is the one that changes agent behaviour โ€” it's cheap
120
+ enough to call before writing the code, which is the only time the answer is free.
121
+
122
+ ```
123
+ > check_api_declaration("UserDefaults", store="apple")
124
+
125
+ ๐Ÿ›‘ BLOCKER ยท status enforced โ€” enforced since 2024-05-01 (806 days ago)
126
+
127
+ You must declare: NSPrivacyAccessedAPICategoryUserDefaults in PrivacyInfo.xcprivacy
128
+
129
+ Valid values:
130
+ ยท CA92.1 โ€” Read/write info accessible only to the app itself...
131
+ ยท 1C8F.1 โ€” ...only to apps, extensions and App Clips in the same App Group...
132
+ ยท C56D.1 โ€” Third-party SDK only: wrapper function around UserDefaults...
133
+ ยท AC6B.1 โ€” Read com.apple.configuration.managed (MDM managed app config)...
134
+ ```
135
+
136
+ ---
137
+
138
+ ## CI
139
+
140
+ ```yaml
141
+ - run: pipx install store-preflight-mcp
142
+ - run: store-preflight scan . --format sarif -o preflight.sarif
143
+ - uses: github/codeql-action/upload-sarif@v3
144
+ with: { sarif_file: preflight.sarif }
145
+ ```
146
+
147
+ SARIF means findings render as inline PR annotations, on the line that caused
148
+ them. `--fail-on blocker` (the default) exits non-zero, so a rejection becomes a
149
+ red check instead of an email.
150
+
151
+ ---
152
+
153
+ ## Coverage
154
+
155
+ **App Store**
156
+ - All **5** required-reason API categories, all **17** reason codes, each with
157
+ Apple's verbatim meaning โ€” `UserDefaults`, `FileTimestamp`, `SystemBootTime`,
158
+ `DiskSpace`, `ActiveKeyboards`
159
+ - **17** `Info.plist` purpose strings, incl. the iOS 17 calendar split
160
+ (`NSCalendarsFullAccessUsageDescription`) most projects still miss
161
+ - Privacy manifest validity: ITMS-91054 / 91055 / 91056
162
+ - All **86** privacy-impacting SDKs + npm/pub โ†’ framework aliasing
163
+ - `NSPrivacyTracking` without `NSPrivacyTrackingDomains`
164
+ - The Xcode 26 / iOS 26 SDK upload gate (since 2026-04-28)
165
+ - All **6** ITMS rejection codes decoded
166
+
167
+ **Google Play**
168
+ - Restricted permissions: `QUERY_ALL_PACKAGES`, `MANAGE_EXTERNAL_STORAGE`,
169
+ SMS/Call Log, `ACCESS_BACKGROUND_LOCATION`, `REQUEST_INSTALL_PACKAGES`,
170
+ `USE_EXACT_ALARM`, `USE_FULL_SCREEN_INTENT`, accessibility, photo/video
171
+ - **14** data safety categories with their data types
172
+ - All **14** foreground service types (including `mediaProcessing`, which most
173
+ lists miss), each with its required permission and runtime prerequisite
174
+ - Advertising ID, including the auto-merged-from-`play-services-ads` trap
175
+ - **New for 2026:** the Contacts policy (2026-10-28) and the geofencing
176
+ withdrawal (2026-08-26)
177
+
178
+ **Languages:** Swift, Objective-C, Kotlin, Java, JS/TS, Dart โ€” plus
179
+ `Info.plist`, `PrivacyInfo.xcprivacy`, `AndroidManifest.xml`, `package.json`,
180
+ `pubspec.yaml`, `Podfile`. React Native, Flutter and native all work; no build
181
+ required.
182
+
183
+ ---
184
+
185
+ ## On being trustworthy
186
+
187
+ A preflight tool that cries wolf gets `--ignore`d within a week, and then it may
188
+ as well not exist. Three deliberate concessions:
189
+
190
+ **Findings carry their own epistemics.** Apple's required-reason rules are
191
+ mechanically checkable: the symbol is in your binary or it isn't. Play's data
192
+ safety form is *not* โ€” it keys on **transmission off-device**, not on holding a
193
+ permission. Google says plainly that on-device processing needs no disclosure. So
194
+ `READ_CONTACTS` does **not** imply a Contacts disclosure, and every such mapping
195
+ is marked `heuristic`: it raises a question, and **a heuristic finding can never
196
+ fail your build** (enforced by a test).
197
+
198
+ **The target API level is not hardcoded.** When this ruleset was built
199
+ (2026-07-15), Google's own timeline page was internally inconsistent โ€” metadata
200
+ dated May 2026, content still describing the August 2025 / API 35 deadline โ€” and
201
+ *no API 36 announcement existed on any Google property*, despite third-party
202
+ blogs confidently asserting one. So the **rule** is encoded ("within one year of
203
+ the latest major Android release") along with `last_verified` and a
204
+ `needs-live-check` flag. A tool that states a fake deadline confidently is worse
205
+ than one that says "verify this".
206
+
207
+ **The SDK check is a prompt, not a verdict.** Apple's requirement attaches when
208
+ an app is new or an update *adds* a listed SDK โ€” not to every app that contains
209
+ Firebase. And "any SDKs that repackage those on the list" are included, so exact
210
+ matching under-detects. Both facts make a confident verdict impossible, so it
211
+ ships as a warning that says why.
212
+
213
+ What we do *not* concede: comments and string literals are stripped before symbol
214
+ matching (with line numbers preserved), `node_modules`/`Pods` are never walked,
215
+ and `tools:node="remove"` is honoured โ€” so the correct way to drop the
216
+ auto-merged `AD_ID` permission isn't reported as the violation.
217
+
218
+ ---
219
+
220
+ ## Verify it
221
+
222
+ ```bash
223
+ python smoke_test.py
224
+ ```
225
+
226
+ 70 assertions, no network. Four fixture repos: one that gets rejected, the same
227
+ app fixed, a manifest that is wrong on its own terms, and one whose code never
228
+ changes while the policy date moves under it. The suite also pins the
229
+ facts that are easy to get wrong:
230
+
231
+ ```
232
+ โœ“ exactly 5 required-reason categories
233
+ โœ“ exactly 17 reason codes
234
+ โœ“ 86 SDKs on the commonly-used list
235
+ โœ“ getattrlist maps to BOTH FileTimestamp and DiskSpace (not 1:1)
236
+ โœ“ ITMS-91054 title is 'Invalid API category declaration'
237
+ โœ“ 14 foreground service types incl. mediaProcessing
238
+ โœ“ target SDK is a rule, not a hardcoded literal
239
+ โœ“ no heuristic finding is ever a blocker
240
+ ```
241
+
242
+ Those aren't padding โ€” each one is a mistake that was actually made and caught
243
+ during the build. `getattrlist` really does oblige two separate declarations. The
244
+ SDK list really is 86, not 87, because Apple publishes "BoringSSL / openssl_grpc"
245
+ as one entry.
246
+
247
+ See [RULES.md](RULES.md) for provenance and how to re-verify against Apple's
248
+ JSON feed.
249
+
250
+ ## Licence
251
+
252
+ MIT