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 +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/appstore-precheck/SKILL.md +8 -2
- package/skills/appstore-precheck/guidelines-baseline.json +9 -8
- package/skills/appstore-precheck/guidelines-fingerprints.json +233 -0
- package/skills/appstore-precheck/references/methodology.md +8 -1
- package/skills/appstore-precheck/references/pierre-deep-review.md +2 -2
- package/skills/appstore-precheck/scripts/project-model.sh +190 -0
- package/skills/appstore-precheck/scripts/scan.sh +19 -5
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|
|
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"
|
|
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.
|
|
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.
|
|
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.
|
|
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" ]] && {
|
|
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" ]]
|
|
133
|
-
echo "$alt"
|
|
145
|
+
if [[ -n "$best" ]]; then IOS_DIR="$best"; else IOS_DIR="$alt"; fi
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
|
|
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
|