appstore-precheck 1.6.0 → 1.8.0

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,65 @@ All notable changes to this project are documented here. Versioning follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.8.0] - 2026-07-02
9
+
10
+ Smarter analysis (roadmap #2c): a deterministic **semantic guideline-drift**
11
+ detector. Beyond the existing section-number check, the tool now fingerprints the
12
+ *text* of every guideline section our checks depend on and reports when Apple
13
+ rewrites a section's meaning (even if its number is unchanged), naming the
14
+ affected check(s). It is a maintainer/CI tool — network-using (`curl`), never
15
+ sourced by `scan.sh`, never in the offline user scan path. Zero new runtime
16
+ dependency for the distributed scanner; default scan output stays byte-identical.
17
+
18
+ ### Added
19
+ - **`scripts/guideline-drift.sh`** (maintainer/CI): fetches the full live App
20
+ Store Review Guidelines and reports section-number drift AND text drift of
21
+ covered sections, deriving the affected check(s) from `scan.sh`'s `set_rule`
22
+ blocks. Non-blocking (WARN lines, always exits 0); the parse/diff logic is
23
+ unit-tested offline via `--html`. `--reconcile` regenerates the fingerprint
24
+ baseline (a deliberate human step) and skips/ WARNs on sections Apple has
25
+ removed rather than writing empty placeholders.
26
+ - **`skills/appstore-precheck/guidelines-fingerprints.json`** — human-reconciled
27
+ per-covered-section text fingerprints (57 sections).
28
+ - A non-blocking scheduled + manual GitHub workflow (`guideline-drift.yml`) that
29
+ surfaces drift; it never gates a PR.
30
+
31
+ ### Changed
32
+ - **Baseline reconciled to Apple's current guidelines** (the drift tool's first
33
+ run caught real drift): Apple consolidated section 5.6 to end at **5.6.4** —
34
+ the removed **5.6.5–5.6.7** were dropped from the baseline and Pierre
35
+ deep-review check 28 (rating/review-manipulation) was remapped to the surviving
36
+ **5.6.1 / 5.6.3**. Six sub-sections a prior truncated fetch had missed
37
+ (2.5.7, 2.5.10, 4.2.4, 4.2.5, 4.4.3, 4.6) were added to `all_sections`.
38
+
39
+ ## [1.7.0] - 2026-07-02
40
+
41
+ Smarter analysis (roadmap #2b): the scanner now resolves the iOS source directory
42
+ and `Info.plist` from the Xcode **project model** (`.pbxproj`) instead of a pure
43
+ grep heuristic, eliminating the dominant remaining false-positive source in
44
+ monorepo / SPM / multi-target layouts. Zero new runtime dependencies (pure
45
+ bash + awk), READ-ONLY preserved, and default text output stays byte-identical for
46
+ any repo without a `.pbxproj`.
47
+
48
+ ### Added
49
+ - **Project-model detection** (`skills/appstore-precheck/scripts/project-model.sh`):
50
+ parses `.pbxproj` to find the primary `application`-type target and resolve its
51
+ source dir + `INFOPLIST_FILE` authoritatively, across ALL `.xcodeproj` in a
52
+ monorepo. `detect_ios_dir` now chains: config `.iosSourceDir` > project-model
53
+ parse > the original grep heuristic (unchanged, kept as fallback).
54
+ - Per-target `INFOPLIST_FILE` attribution via the build-config graph
55
+ (target → `buildConfigurationList` → `XCBuildConfiguration`), used as a last
56
+ resort so already-correct apps are untouched; unexpanded build-variable paths
57
+ (`$(SRCROOT)` etc.) are guarded, and app targets under vendored paths
58
+ (`ThirdParty`/`Vendor`) are deprioritized so a vendored sample app never wins.
59
+
60
+ ### Measured impact (18-app open-source panel, candidate/directional labels)
61
+ - Corrects detection on `wikipedia-ios`, `pocket-casts-ios`, `cwa-app-ios`
62
+ (now read their real custom-named plists) and `brave-ios` (real app over a
63
+ vendored sample). `usage-description-crosscheck` false positives 9 → 6, with
64
+ content-grounded findings surfaced by finally reading the correct plist; zero
65
+ true-positive loss. See `docs/fp-reduction-report.md`.
66
+
8
67
  ## [1.6.0] - 2026-07-01
9
68
 
10
69
  Measurement release: structured findings, suppression, and a published
package/README.md CHANGED
@@ -132,7 +132,7 @@ signals with a higher false-positive rate; Pierre prefers `not applicable` when
132
132
  | **5.2.1–5.2.3** | Obvious third-party trademark or brand misuse in metadata or UI copy |
133
133
  | **5.3.1–5.3.3** | Contest / sweepstakes copy includes official rules and eligibility |
134
134
  | **5.6.2–5.6.3** | Developer identity consistent (app name, support URL content, domains) |
135
- | **5.6.4–5.6.7** † | Rating / review manipulation dark patterns (withhold features until 5 stars, write-review links without `requestReview`) |
135
+ | **5.6.1 / 5.6.3** † | Rating / review manipulation dark patterns (withhold features until 5 stars, write-review links without `requestReview`) |
136
136
 
137
137
  Pierre runs **all 28 every time** and reports each as `REVIEW-PASS:` or `REVIEW-FINDING:`. When the
138
138
  static scan already flagged a guideline, the deep check adds semantic context the scanner could not see.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-precheck",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill with native Claude Code, Cursor, and Codex plugins, plus an npx CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Berkay Turk (https://github.com/berkayturk)",
@@ -4,7 +4,7 @@ description: Read-only pre-submission check for an iOS app before App Store revi
4
4
  license: MIT
5
5
  metadata:
6
6
  author: Berkay Turk
7
- version: 1.6.0
7
+ version: 1.8.0
8
8
  allowed-tools: Bash Read Grep Glob WebFetch
9
9
  ---
10
10
 
@@ -104,6 +104,12 @@ two-pass technique; the exact prompts and the reconciliation procedure are in
104
104
  [`references/methodology.md`](references/methodology.md#phase-0-guideline-drift-check). The
105
105
  baseline is **never auto-updated**. Reconciliation is a deliberate human step.
106
106
 
107
+ The deterministic, full-page drift detector is `scripts/guideline-drift.sh` (maintainer/CI;
108
+ curl-based, so it also covers the 5.5–5.6.x tail that `WebFetch` truncates). It reports
109
+ section-number drift AND text (semantic) drift of covered sections, naming the affected
110
+ check(s). It is non-blocking and never runs in the offline user scan; the fingerprint baseline
111
+ (`guidelines-fingerprints.json`) is human-reconciled via `guideline-drift.sh --reconcile`.
112
+
107
113
  ### Phase 1: Static scan
108
114
 
109
115
  ```bash
@@ -265,7 +271,7 @@ SDK usage, screenshots vs features, and paywall disclosure quality.
265
271
  | 25 | **5.2.1–5.2.3** | Obvious trademark/brand misuse in metadata or UI? |
266
272
  | 26 | **5.3.1–5.3.3** | Contest/sweepstakes copy includes official rules? |
267
273
  | 27 | **5.6.2–5.6.3** | Developer identity consistent (support URL, domains, app name)? |
268
- | 28 † | **5.6.4–5.6.7** | Rating manipulation dark patterns beyond scan §25? |
274
+ | 28 † | **5.6.1 / 5.6.3** | Rating manipulation dark patterns beyond scan §25? |
269
275
 
270
276
  Use this prompt after Phase 3:
271
277
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "_doc": "Baseline of Apple App Store Review Guidelines section numbers, for drift detection. Phase 0 fetches the live guidelines page and diffs its section numbers against `all_sections`; any NEW/REMOVED number => WARN: guideline-drift. This file is updated ONLY by deliberate human reconciliation (see SKILL.md Phase 0), NEVER auto-edited by the skill, otherwise the warning would be silently swallowed.",
3
3
  "source_url": "https://developer.apple.com/app-store/review/guidelines/",
4
- "reconciled_on": "2026-06-30",
5
- "apple_last_updated_seen": "Last Updated: June 8, 2026 (now exposed at the page footer); drift signal remains the set of section numbers",
4
+ "reconciled_on": "2026-07-02",
5
+ "apple_last_updated_seen": "Last Updated: June 8, 2026 (now exposed at the page footer); drift signal remains the set of section numbers. 2026-07-02: confirmed 5.6 now ends at 5.6.4 (5.6.1 App Store Reviews, 5.6.2 Developer Identity, 5.6.3 Discovery Fraud, 5.6.4 App Quality) — 5.6.5/5.6.6/5.6.7 (rating/review manipulation) were removed/consolidated; that content now lives under 5.6.1 and 5.6.3.",
6
6
  "drift_check_coverage": "Reliable for Sections 1-4 + 5.1-5.4 (high-churn regions: 2.5.x, 3.1.x, 4.7, 5.1.x). The 5.5-5.6.x tail can't be fetched (the page truncates ~5.4) and is structurally stable; review it by hand at reconciliation.",
7
7
  "covered_by_scan": [
8
8
  "1.2", "1.5", "1.6",
@@ -21,7 +21,7 @@
21
21
  "5.1.1", "5.1.2", "5.1.3", "5.1.4", "5.4",
22
22
  "5.2.1", "5.2.2", "5.2.3",
23
23
  "5.3.1", "5.3.2", "5.3.3",
24
- "5.6.2", "5.6.3", "5.6.4", "5.6.5", "5.6.6", "5.6.7"
24
+ "5.6.1", "5.6.2", "5.6.3", "5.6.4"
25
25
  ],
26
26
  "all_sections": [
27
27
  "1.1", "1.1.1", "1.1.2", "1.1.3", "1.1.4", "1.1.5", "1.1.6", "1.1.7",
@@ -30,18 +30,19 @@
30
30
  "2.1", "2.2", "2.3", "2.3.1", "2.3.2", "2.3.3", "2.3.4", "2.3.5", "2.3.6",
31
31
  "2.3.7", "2.3.8", "2.3.9", "2.3.10", "2.3.11", "2.3.12", "2.3.13",
32
32
  "2.4", "2.4.1", "2.4.2", "2.4.3", "2.4.4", "2.4.5",
33
- "2.5", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.5.8", "2.5.9",
34
- "2.5.11", "2.5.12", "2.5.13", "2.5.14", "2.5.15", "2.5.16", "2.5.17", "2.5.18",
33
+ "2.5", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.5.7", "2.5.8", "2.5.9",
34
+ "2.5.10", "2.5.11", "2.5.12", "2.5.13", "2.5.14", "2.5.15", "2.5.16", "2.5.17", "2.5.18",
35
35
  "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5",
36
36
  "3.2", "3.2.1", "3.2.2",
37
- "4.1", "4.2", "4.2.1", "4.2.2", "4.2.3", "4.2.6", "4.2.7",
38
- "4.3", "4.4", "4.4.1", "4.4.2",
37
+ "4.1", "4.2", "4.2.1", "4.2.2", "4.2.3", "4.2.4", "4.2.5", "4.2.6", "4.2.7",
38
+ "4.3", "4.4", "4.4.1", "4.4.2", "4.4.3",
39
39
  "4.5", "4.5.1", "4.5.2", "4.5.3", "4.5.4", "4.5.5", "4.5.6",
40
+ "4.6",
40
41
  "4.7", "4.7.1", "4.7.2", "4.7.3", "4.7.4", "4.7.5",
41
42
  "4.8", "4.9", "4.10",
42
43
  "5.1", "5.1.1", "5.1.2", "5.1.3", "5.1.4", "5.1.5",
43
44
  "5.2", "5.2.1", "5.2.2", "5.2.3", "5.2.4", "5.2.5",
44
45
  "5.3", "5.3.1", "5.3.2", "5.3.3", "5.3.4",
45
- "5.4", "5.5", "5.6", "5.6.1", "5.6.2", "5.6.3", "5.6.4", "5.6.5", "5.6.6", "5.6.7"
46
+ "5.4", "5.5", "5.6", "5.6.1", "5.6.2", "5.6.3", "5.6.4"
46
47
  ]
47
48
  }
@@ -0,0 +1,233 @@
1
+ {
2
+ "sections": {
3
+ "1.2": {
4
+ "fingerprint": "ce18f5618681eafe5b4010d3162e0cdfe026aed127295d1762a1fe0c23f68026",
5
+ "snapshot": "1.2 user-generated content apps with user-generated content present particular challenges, ranging from intellectual property infringement to anonymous bullying"
6
+ },
7
+ "1.2.1": {
8
+ "fingerprint": "ffadee3ecbf75b6cf5b293fa2d9f59b2d68c1ba61e6e4c0cb33852324081e84b",
9
+ "snapshot": "1.2.1 creator content apps which feature content from a specific community of users called “creators” are a great opportunity if properly moderated. these apps "
10
+ },
11
+ "1.4.1": {
12
+ "fingerprint": "30045ffe09e8c2fe50923eb5f718ddec394b048bef67c6b7959abadb178c8406",
13
+ "snapshot": "1.4.1 medical apps that could provide inaccurate data or information, or that could be used for diagnosing or treating patients may be reviewed with greater scr"
14
+ },
15
+ "1.5": {
16
+ "fingerprint": "5553347bd97263487f23eae8b7577d036a875ba0934bbde44ac4216218fe85fa",
17
+ "snapshot": "1.5 developer information people need to know how to reach you with questions and support issues. make sure your app and its support url include an easy way to "
18
+ },
19
+ "1.6": {
20
+ "fingerprint": "0620b0807840c795dfcdf546b4a9e648564a192b48a995e3b507f60f26ee690f",
21
+ "snapshot": "1.6 data security apps should implement appropriate security measures to ensure proper handling of user information collected pursuant to the apple developer pr"
22
+ },
23
+ "2.1": {
24
+ "fingerprint": "f5cd98793df128268b4fe5b3b31b006776b0678f6d79d0b88d0d9b3986ea5fd4",
25
+ "snapshot": "2.1 app completeness (a) submissions to app review, including apps you make available for pre-order, should be final versions with all necessary metadata and fu"
26
+ },
27
+ "2.2": {
28
+ "fingerprint": "5df8bc50bf6ef5ce31be95dc03f8bc4ece4cd107055a5124add9336b49471a0d",
29
+ "snapshot": "2.2 beta testing demos, betas, and trial versions of your app don’t belong on the app store – use testflight instead. any app submitted for beta distribution vi"
30
+ },
31
+ "2.3": {
32
+ "fingerprint": "f7b9c31377de7f721ecb9f203a2f107d3600c464ce0633e0d79dd5318250d4df",
33
+ "snapshot": "2.3 accurate metadata customers should know what they’re getting when they download or buy your app, so make sure all your app metadata, including privacy infor"
34
+ },
35
+ "2.3.1": {
36
+ "fingerprint": "4745cb96a6e7665103163daa8a6f07dfc08008288116132a1f3c668d621c61b3",
37
+ "snapshot": "2.3.1 (a) don’t include any hidden, dormant, or undocumented features in your app; your app’s functionality should be clear to end users and app review. all new"
38
+ },
39
+ "2.3.10": {
40
+ "fingerprint": "a770ee4d270784e3bfa0784aab62563acccae50f02e4d85c4bb65bda14a4133d",
41
+ "snapshot": "2.3.10 make sure your app is focused on the experience of the apple platforms it supports, and don’t include names, icons, or imagery of other mobile platforms "
42
+ },
43
+ "2.3.11": {
44
+ "fingerprint": "393e4075d4b403ff201771e040768963299b7df6f01693317ea87c208276b63a",
45
+ "snapshot": "2.3.11 apps you submit for pre-order on the app store must be complete and deliverable as submitted. ensure that the app you ultimately release is not materiall"
46
+ },
47
+ "2.3.12": {
48
+ "fingerprint": "c4fe778434d20e2157b2093ceab8e780d6a98f85c05c57823507992f58de54a3",
49
+ "snapshot": "2.3.12 apps must clearly describe new features and product changes in their “what’s new” text. simple bug fixes, security updates, and performance improvements "
50
+ },
51
+ "2.3.13": {
52
+ "fingerprint": "40d66f89efc57625525782e40afc24aabd8a4cbe2a745eca725a7a2d6324f742",
53
+ "snapshot": "2.3.13 in-app events are timely events that happen within your app. to feature your event on the app store, it must fall within an event type provided in app st"
54
+ },
55
+ "2.3.2": {
56
+ "fingerprint": "e070f3780bbbbf41f191f71ef396904f9d42d531ab782f6f35b4e0c9b95dbf0a",
57
+ "snapshot": "2.3.2 if your app includes in-app purchases, make sure your app description, screenshots, and previews clearly indicate whether any featured items, levels, subs"
58
+ },
59
+ "2.3.3": {
60
+ "fingerprint": "599fcbd06e68d93bab64101dc412f5c6ffb13eb76e8490c7e93d95d95e4dafae",
61
+ "snapshot": "2.3.3 screenshots should show the app in use, and not merely the title art, login page, or splash screen. they may also include text and image overlays (e.g. to"
62
+ },
63
+ "2.3.4": {
64
+ "fingerprint": "581fe91e7677d96de480c0edca7761023e5606f5d6cd39ae42a06d29081ab393",
65
+ "snapshot": "2.3.4 previews are a great way for customers to see what your app looks like and what it does. to ensure people understand what they’ll be getting with your app"
66
+ },
67
+ "2.3.5": {
68
+ "fingerprint": "b4fb364fef12344219399d477daa73bca645261f6feee1a67dc88916a417c5c5",
69
+ "snapshot": "2.3.5 select the most appropriate category for your app, and check out the app store category definitions if you need help. if you’re way off base, we may chang"
70
+ },
71
+ "2.3.6": {
72
+ "fingerprint": "a6804ee336bd78da1ddc44613ea6fe131b5dcf4cbe482b8cb696ef3720435c3d",
73
+ "snapshot": "2.3.6 answer the age rating questions in app store connect honestly so that your app aligns properly with parental controls. if your app is mis-rated, customers"
74
+ },
75
+ "2.3.7": {
76
+ "fingerprint": "3493119147fdb0b4735ce6760d2a0186a4941531ff209e4198879f0a1c201a2c",
77
+ "snapshot": "2.3.7 choose a unique app name, assign keywords that accurately describe your app, and don’t try to pack any of your metadata with trademarked terms, popular ap"
78
+ },
79
+ "2.3.8": {
80
+ "fingerprint": "9c1bc1fbee57f292a14e4b6cfa465021ad0f7acb79691337b2a0f5c2e41392bb",
81
+ "snapshot": "2.3.8 metadata should be appropriate for all audiences, so make sure your app and in-app purchase icons, screenshots, and previews adhere to a 4+ age rating eve"
82
+ },
83
+ "2.3.9": {
84
+ "fingerprint": "5161fcd6aaa9dfd7b3cfff5f32912448dc937083d095fe9049cef0f80a09fa32",
85
+ "snapshot": "2.3.9 you are responsible for securing the rights to use all materials in your app icons, screenshots, and previews, and you should display fictional account in"
86
+ },
87
+ "2.5.1": {
88
+ "fingerprint": "7b055c4d2438faeaee3b1b77f5ce9b9cec38e713466ea0c85d31117d6484ba8b",
89
+ "snapshot": "2.5.1 apps may only use public apis and must run on the currently shipping os. learn more about public apis . keep your apps up-to-date and make sure you phase "
90
+ },
91
+ "2.5.2": {
92
+ "fingerprint": "353ebb9358966707a33b8b1b29575e44c1a2db44e8a42c013bcd48ca296fec44",
93
+ "snapshot": "2.5.2 apps should be self-contained in their bundles, and may not read or write data outside the designated container area, nor may they download, install, or e"
94
+ },
95
+ "2.5.4": {
96
+ "fingerprint": "9695ae52e1472dc7f5d977a88287c590b468401dd6a1fb30ddbe252029c59c81",
97
+ "snapshot": "2.5.4 multitasking apps may only use background services for their intended purposes: voip, audio playback, location, task completion, local notifications, etc."
98
+ },
99
+ "3.1.1": {
100
+ "fingerprint": "16d14be80e2243e9456be40bbc6c8dfb2cb6e90c6e98e5c36ba6e7f7147a61e8",
101
+ "snapshot": "3.1.1 in-app purchase: if you want to unlock features or functionality within your app, (by way of example: subscriptions, in-game currencies, game levels, acce"
102
+ },
103
+ "3.1.2": {
104
+ "fingerprint": "015f5baa628e085d550f8bc6d47f0cc33508915a5dced6d8b238e6a52616690a",
105
+ "snapshot": "3.1.2 subscriptions: apps may offer auto-renewable in-app purchase subscriptions, regardless of category on the app store. when incorporating auto-renewable sub"
106
+ },
107
+ "3.1.5": {
108
+ "fingerprint": "ff34b24b578bec14defe707f1333593160535ab927d03676f1e533b4d9ef03bb",
109
+ "snapshot": "3.1.5 cryptocurrencies: (i) wallets: apps may facilitate virtual currency storage, provided they are offered by developers enrolled as an organization. (ii) min"
110
+ },
111
+ "4.2": {
112
+ "fingerprint": "381768384e5d58de6c2c75ccfae3524279669ea710b29bd12994561ba002e9d6",
113
+ "snapshot": "4.2 minimum functionality your app should include features, content, and ui that elevate it beyond a repackaged website. if your app is not particularly useful,"
114
+ },
115
+ "4.2.1": {
116
+ "fingerprint": "186350931d2f6ae6faf4fab30b4ad0000cb386890f59dfc1f0d55904e0d31ebc",
117
+ "snapshot": "4.2.1 apps using arkit should provide rich and integrated augmented reality experiences; merely dropping a model into an ar view or replaying animation is not e"
118
+ },
119
+ "4.2.2": {
120
+ "fingerprint": "504f8404893912593ded8f89748de71a847897cc0cf7f987e475323edef8650a",
121
+ "snapshot": "4.2.2 other than catalogs, apps shouldn’t primarily be marketing materials, advertisements, web clippings, content aggregators, or a collection of links."
122
+ },
123
+ "4.2.3": {
124
+ "fingerprint": "60bde0e85a028024660dd1b6da7a487083fea5c8e346e1f9d4ef287e6bce7829",
125
+ "snapshot": "4.2.3 (i) your app should work on its own without requiring installation of another app to function. (ii) if your app needs to download additional resources in "
126
+ },
127
+ "4.2.7": {
128
+ "fingerprint": "debd395643a03b8dec28d32aed55ec883b91ce24df156ed8365846245a56d436",
129
+ "snapshot": "4.2.7 remote desktop clients: if your remote desktop app acts as a mirror of specific software or services rather than a generic mirror of the host device, it m"
130
+ },
131
+ "4.4.1": {
132
+ "fingerprint": "520a1ac7a5d13b54061b05bdc57a851e7d5f541a352144623b16c52ae578e5c6",
133
+ "snapshot": "4.4.1 keyboard extensions have some additional rules. they must: provide keyboard input functionality (e.g. typed characters); follow sticker guidelines if the "
134
+ },
135
+ "4.4.2": {
136
+ "fingerprint": "6b7f6bde5787ba828a1f18833340140d858d1cc520430b0519f487a7d86dc54c",
137
+ "snapshot": "4.4.2 safari extensions must run on the current version of safari on the relevant apple operating system. they may not interfere with system or safari ui elemen"
138
+ },
139
+ "4.5.1": {
140
+ "fingerprint": "9a82e38f4117ddc59f65993ff1ba5c8fba392fb91f7aad3936725e289d348b38",
141
+ "snapshot": "4.5.1 apps may use approved apple rss feeds such as the itunes store rss feed, but may not scrape any information from apple sites (e.g. apple.com, the itunes s"
142
+ },
143
+ "4.5.2": {
144
+ "fingerprint": "285371a2e18e84a6b20f61f4b9e4f342c2890debecfaf527c57051d4e15c48b4",
145
+ "snapshot": "4.5.2 apple music (i) musickit on ios lets users play apple music and their local music library natively from your apps and games. when a user provides permissi"
146
+ },
147
+ "4.5.3": {
148
+ "fingerprint": "66dcdc8015b616f09d2dc81e96567f1e003d3c71d8549e89d911ff1a7a3a528e",
149
+ "snapshot": "4.5.3 do not use apple services to spam, phish, or send unsolicited messages to customers, including game center, push notifications, live activities, etc. do n"
150
+ },
151
+ "4.8": {
152
+ "fingerprint": "4d97c6a408a925e9209c7f01ca4377b17c52eeb837a15023ac776b198f3f203e",
153
+ "snapshot": "4.8 login services apps that use a third-party or social login service (such as facebook login, google sign-in, log in with x, sign in with linkedin, login with"
154
+ },
155
+ "4.9": {
156
+ "fingerprint": "6ac6eb5bc1047758fde6986c12411e5ccd0901ae38c71facf71076dab9a8f671",
157
+ "snapshot": "4.9 apple pay apps using apple pay must provide all material purchase information to the user prior to sale of any good or service and must use apple pay brandi"
158
+ },
159
+ "5.1.1": {
160
+ "fingerprint": "fed9edf9021681ed9f27139e162f963bf4823e95775d6efad1156c59e9bd7baf",
161
+ "snapshot": "5.1.1 data collection and storage (i) privacy policies: all apps must include a link to their privacy policy in the app store connect metadata field and within "
162
+ },
163
+ "5.1.2": {
164
+ "fingerprint": "9817b825aa227ee7959bdecde672a78dffa147ce2ded3e3bcc0c0a4b3010f80c",
165
+ "snapshot": "5.1.2 data use and sharing (i) unless otherwise permitted by law, you may not use, transmit, or share someone’s personal data without first obtaining their perm"
166
+ },
167
+ "5.1.3": {
168
+ "fingerprint": "12a61190ea01b69c11d8d029b473777b5e9590739fd3043afa25f4f0f05c114a",
169
+ "snapshot": "5.1.3 health and health research health, fitness, and medical data are especially sensitive and apps in this space have some additional rules to make sure custo"
170
+ },
171
+ "5.1.4": {
172
+ "fingerprint": "43671205363120d06dd34eb1971c4751e8582fb8f7dfe69abbcadf2cd36195ea",
173
+ "snapshot": "5.1.4 kids (a) for many reasons, it is critical to use care when dealing with personal data from kids, and we encourage you to carefully review all the requirem"
174
+ },
175
+ "5.1.5": {
176
+ "fingerprint": "07911be837521693da654a46f08e84c9ca2ec3cf9ad87c65f54ae9624d157b3e",
177
+ "snapshot": "5.1.5 location services use location services in your app only when it is directly relevant to the features and services provided by the app. location-based api"
178
+ },
179
+ "5.2.1": {
180
+ "fingerprint": "358d2d81a7b375d308077bbb71b07989235f5667d2cda7fffb14ac1b006c7ae8",
181
+ "snapshot": "5.2.1 generally: don’t use protected third-party material such as trademarks, copyrighted works, or patented ideas in your app without permission, and don’t inc"
182
+ },
183
+ "5.2.2": {
184
+ "fingerprint": "b7e091540eb347bfa61de6c24a960913e95e67bfe4b40197a464d56b21c45424",
185
+ "snapshot": "5.2.2 third-party sites/services: if your app uses, accesses, monetizes access to, or displays content from a third-party service, ensure that you are specifica"
186
+ },
187
+ "5.2.3": {
188
+ "fingerprint": "af448918a1af7645066c9cce2be68952a841908a49efe143123b5641e8cf2215",
189
+ "snapshot": "5.2.3 audio/video downloading: apps should not facilitate illegal file sharing or include the ability to save, convert, or download media from third-party sourc"
190
+ },
191
+ "5.3.1": {
192
+ "fingerprint": "b1b9f19d97e0303c2b658f1b7d9d0db70dc417bdff7ce826c1e5b93b4a35ca55",
193
+ "snapshot": "5.3.1 sweepstakes and contests must be sponsored by the developer of the app."
194
+ },
195
+ "5.3.2": {
196
+ "fingerprint": "49577a0affde1ea6a425b47803c251eaede1565ee4a92676587bda910b5f0632",
197
+ "snapshot": "5.3.2 official rules for sweepstakes, contests, and raffles must be presented in the app and make clear that apple is not a sponsor or involved in the activity "
198
+ },
199
+ "5.3.3": {
200
+ "fingerprint": "7770603fbb406eeaa5d1320ca6baa5f57a72ee680200d82373bbc9dd6e41b8fe",
201
+ "snapshot": "5.3.3 apps may not use in-app purchase to purchase credit or currency for use in conjunction with real money gaming of any kind."
202
+ },
203
+ "5.3.4": {
204
+ "fingerprint": "aaca06600d77f4c3d63ffd9c6c3f5eef979135c2ce3f1263bb5349420d036387",
205
+ "snapshot": "5.3.4 apps that offer real money gaming (e.g. sports betting, poker, casino games, horse racing) or lotteries must have necessary licensing and permissions in t"
206
+ },
207
+ "5.4": {
208
+ "fingerprint": "3d639b21b98c7d15167fdfbd71559a91a5de51a7b5c22f300203e5f341af8616",
209
+ "snapshot": "5.4 vpn apps apps offering vpn services must utilize the nevpnmanager api and may only be offered by developers enrolled as an organization. you must make a cle"
210
+ },
211
+ "5.5": {
212
+ "fingerprint": "f8c3db6e7237d7c5a5002a507e20296153e906118b7ce05c537d1873e2adeab0",
213
+ "snapshot": "5.5 mobile device management mobile device management apps that offer mobile device management (mdm) services must request this capability from apple. such apps"
214
+ },
215
+ "5.6.1": {
216
+ "fingerprint": "17224df537dcc818fd69678e5f067156127cc8513cb0188ad6e3431641673172",
217
+ "snapshot": "5.6.1 app store reviews app store customer reviews can be an integral part of the app experience, so you should treat customers with respect when responding to "
218
+ },
219
+ "5.6.2": {
220
+ "fingerprint": "960f0c0dab8442a42b03926ae1123ffabbc676ecd4dc1890d31fd58c3ba9cf36",
221
+ "snapshot": "5.6.2 developer identity providing verifiable information to apple and customers is critical to customer trust. your representation of yourself, your business, "
222
+ },
223
+ "5.6.3": {
224
+ "fingerprint": "1d9e3acfb31ec0ba65a77d09e230f5a3c408bc4438343437aa8dc3bd17d5967b",
225
+ "snapshot": "5.6.3 discovery fraud participating in the app store requires integrity and a commitment to building and maintaining customer trust. manipulating any element of"
226
+ },
227
+ "5.6.4": {
228
+ "fingerprint": "2f63efa4160b309b6ef4d1280e3d16a06165676f1421c3c36f6dbe305ca1291a",
229
+ "snapshot": "5.6.4 app quality customers expect the highest quality from the app store, and maintaining high quality content, services, and experiences promotes customer tru"
230
+ }
231
+ },
232
+ "reconciled_on": "2026-07-02"
233
+ }
@@ -52,6 +52,13 @@ number is unchanged but whose *text* changed is not caught here, but is partly c
52
52
  Phase 2 (Apple's own rule engine) and Phase 3 (Pierre explains every FAIL/WARN). Apple does not expose a
53
53
  machine-readable "last updated" date in the page DOM, so the section-number set is the signal.
54
54
 
55
+ **Automated detector:** `scripts/guideline-drift.sh` is the deterministic, curl-based counterpart
56
+ to the two-pass technique above — it runs unattended in CI (see
57
+ `.github/workflows/guideline-drift.yml`, scheduled + `workflow_dispatch`, always non-blocking) and
58
+ also covers text (semantic) drift of covered sections via `guidelines-fingerprints.json`, naming
59
+ the affected check(s) when a section's fingerprint no longer matches. Reconcile the fingerprint
60
+ baseline the same deliberate way, with `guideline-drift.sh --reconcile`.
61
+
55
62
  ---
56
63
 
57
64
  ## Phase 1: Rejection vectors
@@ -147,7 +154,7 @@ finds; Pierre explains.
147
154
  After Phase 3, Pierre runs the **Review Simulator**: 28 evidence-based checks the static scanner
148
155
  cannot fully judge — **22 Tier A** (high-confidence) plus **6 Tier B v1** heuristic checks (items
149
156
  **4, 5, 7, 10, 15, 28** in the checklist: 2.1 review notes, 2.2, 2.3.4, 2.3.9, 4.5.1–4.5.3,
150
- 5.6.4–5.6.7). Full procedure, output format, and per-check steps are in
157
+ 5.6.1/5.6.3). Full procedure, output format, and per-check steps are in
151
158
  [`pierre-deep-review.md`](pierre-deep-review.md).
152
159
 
153
160
  **Verdict impact:** none. Phase 4 emits `REVIEW-PASS:` or `REVIEW-FINDING: <guideline> WARN — …`.
@@ -87,7 +87,7 @@ REVIEW-PASS: <guideline> — not applicable (<reason>)
87
87
  | 25 | **5.2.1–5.2.3** | Obvious third-party trademark/brand misuse in metadata, assets, or UI copy? | metadata, asset filenames, Swift strings |
88
88
  | 26 | **5.3.1–5.3.3** | Contest/sweepstakes/lottery copy → official rules/eligibility/disclosure present in metadata? | description, keywords, in-app contest UI |
89
89
  | 27 | **5.6.2–5.6.3** | Developer identity consistent: app name, support URL content, bundle/marketing domain match? | fetch support URL, metadata, legal/footer copy |
90
- | 28 | **5.6.4–5.6.7** | Rating/review manipulation dark patterns (withhold features until 5 stars, direct write-review links without `requestReview`)? | Swift, metadata, §25 scan context |
90
+ | 28 | **5.6.1 / 5.6.3** | Rating/review manipulation dark patterns (withhold features until 5 stars, direct write-review links without `requestReview`)? | Swift, metadata, §25 scan context |
91
91
 
92
92
  ---
93
93
 
@@ -253,7 +253,7 @@ REVIEW-PASS: <guideline> — not applicable (<reason>)
253
253
  2. Compare app name, support domain, and privacy policy domain for consistency.
254
254
  3. Flag placeholder support pages or identity mismatch.
255
255
 
256
- ### 28 — 5.6.4–5.6.7 Rating manipulation *(Tier B v1)*
256
+ ### 28 — 5.6.1 / 5.6.3 Rating/review manipulation *(Tier B v1)*
257
257
 
258
258
  1. Grep Swift and metadata for: `itms-apps://` write-review URLs, `apps.apple.com/.../write-review`,
259
259
  "rate 5 stars", "only enable after review", custom star-rating UI tied to App Store review.
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # project-model.sh — resolve the primary app target's source dir + Info.plist from
3
+ # an Xcode project model (.pbxproj), authoritatively and dependency-free.
4
+ # Sourced by scan.sh. Pure bash + awk. READ-ONLY. Bash 3.2 compatible.
5
+
6
+ # pm_app_targets <pbxproj> -> names of targets whose productType is the application type.
7
+ # .pbxproj PBXNativeTarget blocks list `name` before `productType`; the block closes
8
+ # with a bare `};`. Nested lists close with `)`, never `};`, so `};` reliably ends a block.
9
+ pm_app_targets() {
10
+ awk '
11
+ /isa = PBXNativeTarget;/ { in_t=1; name=""; pt=""; next }
12
+ in_t && /^[[:space:]]*name = / {
13
+ l=$0; sub(/^[[:space:]]*name = /,"",l); sub(/;[[:space:]]*$/,"",l); gsub(/^"|"$/,"",l); name=l
14
+ }
15
+ in_t && /^[[:space:]]*productType = / {
16
+ l=$0; sub(/^[[:space:]]*productType = /,"",l); sub(/;[[:space:]]*$/,"",l); gsub(/^"|"$/,"",l); pt=l
17
+ }
18
+ in_t && /^[[:space:]]*};[[:space:]]*$/ {
19
+ if (pt == "com.apple.product-type.application" && name != "") print name
20
+ in_t=0
21
+ }
22
+ ' "$1"
23
+ }
24
+
25
+ # pm_infoplist_files <pbxproj> -> every INFOPLIST_FILE value, unquoted, sorted, deduped.
26
+ pm_infoplist_files() {
27
+ awk '/^[[:space:]]*INFOPLIST_FILE = /{
28
+ l=$0; sub(/^[[:space:]]*INFOPLIST_FILE = /,"",l); sub(/;[[:space:]]*$/,"",l); gsub(/^"|"$/,"",l); print l
29
+ }' "$1" | sort -u
30
+ }
31
+
32
+ # pm_target_infoplist <pbxproj> <target> -> the target's own INFOPLIST_FILE via its
33
+ # build configurations (first non-empty), or non-zero. Handles quoted paths with spaces.
34
+ #
35
+ # A repo may declare INFOPLIST_FILE for several targets whose leading path
36
+ # component does not match the target name (e.g. Client's plist lives under
37
+ # "iOS/Supporting Files"). pm_infoplist_files() alone cannot attribute a given
38
+ # plist to a specific target, so this walks the actual pbxproj graph:
39
+ # PBXNativeTarget "name = X" -> buildConfigurationList = <UUID> -> the
40
+ # XCConfigurationList block for that UUID -> its buildConfigurations list ->
41
+ # each XCBuildConfiguration block, looking for INFOPLIST_FILE.
42
+ #
43
+ # NOTE: this returns the FIRST build config's plist, which is only reliable
44
+ # when all configs agree. Per-configuration plists (Debug vs Release) can
45
+ # legitimately differ (e.g. cwa-app-ios's ENA target), so pm_resolve() only
46
+ # calls this as a LAST RESORT — after the leading-component match and the
47
+ # GENERATE dir-name branch have both failed. Callers must also guard against
48
+ # unexpanded Xcode build variables (e.g. "$(SRCROOT)/...") in the returned
49
+ # path, since those can never resolve to a real file on disk.
50
+ pm_target_infoplist() {
51
+ local pbx="$1" target="$2" cl u ip
52
+ cl="$(awk -v t="$target" '
53
+ /isa = PBXNativeTarget;/{inb=1;nm="";c="";next}
54
+ inb&&/^[[:space:]]*name = /{l=$0;sub(/^[[:space:]]*name = /,"",l);sub(/;[[:space:]]*$/,"",l);gsub(/^"|"$/,"",l);nm=l}
55
+ inb&&/^[[:space:]]*buildConfigurationList = /{l=$0;sub(/^[[:space:]]*buildConfigurationList = /,"",l);sub(/;[[:space:]]*$/,"",l);sub(/ .*/,"",l);c=l}
56
+ inb&&/^[[:space:]]*};[[:space:]]*$/{if(nm==t&&c!=""){print c;exit}inb=0}
57
+ ' "$pbx")"
58
+ [[ -z "$cl" ]] && return 1
59
+ # Match only the block-OPENING line for a UUID (ends in "{"), never a
60
+ # reference to that UUID inside some other list (e.g. the UUID also
61
+ # appears as a bare list item "CFG1 /* Debug */," inside the
62
+ # buildConfigurations array itself, which would otherwise false-match).
63
+ local bcs; bcs="$(awk -v cl="$cl" '
64
+ $0 ~ ("^[[:space:]]*" cl "[[:space:]]") && /\{[[:space:]]*$/ {inb=1}
65
+ inb&&/buildConfigurations = \(/{inl=1;next}
66
+ inb&&inl&&/\)/{exit}
67
+ inb&&inl{l=$0;sub(/\/\*.*/,"",l);gsub(/[[:space:]]/,"",l);sub(/,$/,"",l);if(l!="")print l}
68
+ ' "$pbx")"
69
+ [[ -z "$bcs" ]] && return 1
70
+ for u in $bcs; do
71
+ ip="$(awk -v u="$u" '
72
+ $0 ~ ("^[[:space:]]*" u "[[:space:]]") && /\{[[:space:]]*$/ {inb=1}
73
+ inb&&/^[[:space:]]*INFOPLIST_FILE = /{l=$0;sub(/^[[:space:]]*INFOPLIST_FILE = /,"",l);sub(/;[[:space:]]*$/,"",l);gsub(/^"|"$/,"",l);print l;exit}
74
+ inb&&/^[[:space:]]*};[[:space:]]*$/{exit}
75
+ ' "$pbx")"
76
+ [[ -n "$ip" ]] && { printf '%s\n' "$ip"; return 0; }
77
+ done
78
+ return 1
79
+ }
80
+
81
+ # Vendored dirs whose .xcodeproj must never win detection.
82
+ PM_PRUNE_DIRS='node_modules|Pods|Carthage|\.build|DerivedData|\.git'
83
+
84
+ # App targets whose project lives under a vendored/sample path are deprioritized:
85
+ # they only win when no primary (non-vendored) app target exists. This is a
86
+ # deprioritization heuristic (mirrors scan.sh's NONAPP_TARGET), bounded on
87
+ # purpose: a sample app under a clearly-vendored path (ThirdParty/Vendor)
88
+ # loses to a primary app, but the heuristic deliberately does NOT match
89
+ # Demo/Sample/Example-named dirs, because those often hold a library repo's
90
+ # real deliverable app rather than a throwaway sample (Pods/Carthage/
91
+ # node_modules are already fully pruned by PM_PRUNE_DIRS in
92
+ # pm_find_pbxprojs, so they need not be listed here either).
93
+ PM_SAMPLE_PATH='(^|/)(ThirdParty|Vendor(ed)?)(/|$)'
94
+
95
+ # pm_find_pbxprojs <root> -> all project.pbxproj under *.xcodeproj (pruned), deterministic order.
96
+ pm_find_pbxprojs() {
97
+ local root="${1:-.}"
98
+ find "$root" -name 'project.pbxproj' -path '*.xcodeproj/*' 2>/dev/null \
99
+ | grep -Ev "/($PM_PRUNE_DIRS)/" \
100
+ | LC_ALL=C sort
101
+ }
102
+
103
+ # pm_resolve <root> -> "DIR<TAB>PLIST" (ROOT-relative; PLIST may be empty) for the
104
+ # primary app target, or non-zero with no output.
105
+ #
106
+ # A monorepo may contain several .xcodeproj (samples, sub-projects, the real
107
+ # app). The shallowest one is not necessarily the real app, so every pbxproj
108
+ # is resolved on its own terms (against its own SRCROOT-relative projdir) and
109
+ # the single global best — most *.swift sources — wins across all of them.
110
+ pm_resolve() {
111
+ local root="${1:-.}" pbx rel projdir apps plists app plist dir n cand_dir cand_plist
112
+ # "best" is the primary (non-vendored/sample) bucket; "alt" collects app
113
+ # targets whose project lives under a vendored/sample path (PM_SAMPLE_PATH).
114
+ # alt only wins when no primary candidate exists at all — see the tail.
115
+ local best="" best_plist="" best_n=-1
116
+ local alt="" alt_plist="" alt_n=-1
117
+ local found=0
118
+ # Normalize a trailing slash so the ROOT-relative strip below reliably matches
119
+ # (root="/" strips to "" and must stay "/"; root="." is untouched).
120
+ root="${root%/}"; [[ -z "$root" ]] && root="/"
121
+ while IFS= read -r pbx; do
122
+ [[ -z "$pbx" ]] && continue
123
+ apps="$(pm_app_targets "$pbx")"; [[ -z "$apps" ]] && continue
124
+ found=1
125
+ # ROOT-relative dir that contains this .xcodeproj (SRCROOT). Its
126
+ # INFOPLIST_FILE paths and app source dir are relative to this.
127
+ rel="${pbx#"$root"/}" # e.g. ios/App.xcodeproj/project.pbxproj
128
+ projdir="$(dirname "$(dirname "$rel")")"; projdir="${projdir#./}"
129
+ [[ "$projdir" == "." ]] && projdir=""
130
+ plists="$(pm_infoplist_files "$pbx")"
131
+ while IFS= read -r app; do
132
+ [[ -z "$app" ]] && continue
133
+ # Priority 1: a declared plist whose leading path component equals the
134
+ # app target name.
135
+ plist="$(printf '%s\n' "$plists" | awk -v a="$app" -F/ '$1==a{print; exit}')"
136
+ if [[ -n "$plist" ]]; then
137
+ dir="$(dirname "$plist")"
138
+ else
139
+ # Priority 2 (GENERATE_INFOPLIST_FILE): no leading-component plist.
140
+ # Use the dir named after the target.
141
+ dir="$(cd "$root${projdir:+/$projdir}" 2>/dev/null && \
142
+ find . -type d -name "$app" 2>/dev/null | sed 's#^\./##' \
143
+ | awk '{print length, $0}' | sort -n | head -1 | cut -d' ' -f2-)"
144
+ if [[ -z "$dir" ]]; then
145
+ # Priority 3 (LAST RESORT — only reached when both of the above
146
+ # fail, i.e. the target would otherwise be skipped entirely):
147
+ # attribute the target's OWN INFOPLIST_FILE via the pbxproj's
148
+ # build-config graph. Works even when the plist's leading path
149
+ # component doesn't match the target name (e.g. brave-ios's
150
+ # Client -> "iOS/Supporting Files/Info.plist"), but must not
151
+ # override a working leading-component or GENERATE resolution
152
+ # (e.g. eigen's Artsy, cwa-app-ios's ENA), so it stays last.
153
+ plist="$(pm_target_infoplist "$pbx" "$app" 2>/dev/null)"
154
+ # A literal, unexpanded Xcode build variable (e.g. $(SRCROOT),
155
+ # $(PROJECT_DIR)) can never be found on disk — treat it as
156
+ # unusable rather than resolving to a broken path.
157
+ [[ "$plist" == *'$('* ]] && plist=""
158
+ if [[ -n "$plist" ]]; then
159
+ dir="$(dirname "$plist")"
160
+ else
161
+ continue
162
+ fi
163
+ fi
164
+ fi
165
+ n="$(cd "$root${projdir:+/$projdir}" 2>/dev/null && \
166
+ find "$dir" -name '*.swift' 2>/dev/null | wc -l | tr -d ' ')"
167
+ # Prefix the projdir so paths are ROOT-relative before comparing/storing.
168
+ cand_dir="${projdir:+$projdir/}$dir"
169
+ cand_plist=""
170
+ [[ -n "$plist" ]] && cand_plist="${projdir:+$projdir/}$plist"
171
+ if [[ "$projdir" =~ $PM_SAMPLE_PATH ]]; then
172
+ if (( n > alt_n )); then
173
+ alt_n=$n; alt="$cand_dir"; alt_plist="$cand_plist"
174
+ fi
175
+ else
176
+ if (( n > best_n )); then
177
+ best_n=$n; best="$cand_dir"; best_plist="$cand_plist"
178
+ fi
179
+ fi
180
+ done <<< "$apps"
181
+ done <<< "$(pm_find_pbxprojs "$root")"
182
+ [[ $found -eq 0 ]] && return 1
183
+ if [[ -n "$best" ]]; then
184
+ printf '%s\t%s\n' "$best" "$best_plist"
185
+ elif [[ -n "$alt" ]]; then
186
+ printf '%s\t%s\n' "$alt" "$alt_plist"
187
+ else
188
+ return 1
189
+ fi
190
+ }
@@ -14,6 +14,7 @@ cd "$ROOT" || { echo "FAIL: repo-root — could not enter repository root"; exit
14
14
 
15
15
  source "$(dirname "${BASH_SOURCE[0]}")/findings.sh"
16
16
  source "$(dirname "${BASH_SOURCE[0]}")/suppress.sh"
17
+ source "$(dirname "${BASH_SOURCE[0]}")/project-model.sh"
17
18
  FINDINGS_TMP="$(mktemp)"; export FINDINGS_TMP
18
19
  trap 'rm -f "$FINDINGS_TMP"' EXIT
19
20
  FORMAT="text"
@@ -109,9 +110,21 @@ NONAPP_TARGET='(Watch|Extension|Widget|Intents|Clip|Notification|Share|Sticker|T
109
110
  # can land on a Watch app, an app extension, or a framework instead of the real app.
110
111
  # We score candidates by Swift-file count and deprioritize obvious non-app targets, so
111
112
  # they only win when nothing app-like exists.
113
+ # Sets the globals IOS_DIR (and, when resolved via the project model, PM_INFO_PLIST)
114
+ # directly rather than echoing a result — this function MUST be called without
115
+ # command substitution (no `IOS_DIR="$(detect_ios_dir)"`) so its assignments run
116
+ # in the caller's shell instead of a subshell, where they would be discarded.
112
117
  detect_ios_dir() {
113
118
  local d; d=$(cfg '.iosSourceDir')
114
- [[ -n "$d" ]] && { echo "$d"; return; }
119
+ [[ -n "$d" ]] && { IOS_DIR="$d"; return; }
120
+ # Authoritative: parse the Xcode project model when a .pbxproj exists.
121
+ local pm; pm="$(pm_resolve . 2>/dev/null)"
122
+ if [[ -n "$pm" ]]; then
123
+ PM_INFO_PLIST="$(printf '%s' "$pm" | cut -f2)"
124
+ IOS_DIR="$(printf '%s' "$pm" | cut -f1)"
125
+ return
126
+ fi
127
+ # Fallback: the original grep heuristic (unchanged).
115
128
  local candidates plist entry
116
129
  candidates=$(
117
130
  find . "${PRUNE[@]}" -name Info.plist 2>/dev/null | while IFS= read -r plist; do dirname "$plist"; done
@@ -129,17 +142,18 @@ detect_ios_dir() {
129
142
  (( n > best_n )) && { best_n=$n; best="$dir"; }
130
143
  fi
131
144
  done <<< "$candidates"
132
- [[ -n "$best" ]] && { echo "$best"; return; }
133
- echo "$alt"
145
+ if [[ -n "$best" ]]; then IOS_DIR="$best"; else IOS_DIR="$alt"; fi
134
146
  }
135
147
 
136
- IOS_DIR="$(detect_ios_dir)"
148
+ PM_INFO_PLIST=""
149
+ IOS_DIR=""
150
+ detect_ios_dir
137
151
  META_DIR="$(cfg '.metadataDir')"; [[ -z "$META_DIR" ]] && META_DIR="$(detect_first -type d -name metadata -path '*fastlane*')"
138
152
  SCREEN_DIR="$(cfg '.screenshotsDir')"; [[ -z "$SCREEN_DIR" ]] && SCREEN_DIR="$(detect_first -type d -name screenshots -path '*fastlane*')"
139
153
  XCSTRINGS="$(cfg '.xcstringsPath')"; [[ -z "$XCSTRINGS" ]] && XCSTRINGS="$(detect_first -name 'Localizable.xcstrings')"
140
154
  [[ -z "$XCSTRINGS" ]] && XCSTRINGS="$(detect_first -name '*.xcstrings')"
141
155
  PRIVACY_FILE="$(detect_first -name 'PrivacyInfo.xcprivacy')"
142
- INFO_PLIST="${IOS_DIR%/}/Info.plist"
156
+ INFO_PLIST="${PM_INFO_PLIST:-${IOS_DIR%/}/Info.plist}"
143
157
  REVIEW_PREP="$(cfg '.reviewPrepNotes')"
144
158
 
145
159
  # Paywall / subscription views. A real app spreads its purchase UI across several files