novaforge-appkit 0.1.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/LICENSE +21 -0
- package/README.md +131 -0
- package/adapters/claude-code/README.md +25 -0
- package/adapters/codex/AGENTS.section.md +53 -0
- package/adapters/codex/README.md +20 -0
- package/bin/appkit.js +171 -0
- package/install.sh +131 -0
- package/kit/agents/builder-agent.md +49 -0
- package/kit/agents/product-agent.md +48 -0
- package/kit/agents/release-agent.md +46 -0
- package/kit/agents/reviewer-agent.md +42 -0
- package/kit/commands/build.md +30 -0
- package/kit/commands/fix.md +33 -0
- package/kit/commands/idea.md +28 -0
- package/kit/commands/init.md +29 -0
- package/kit/commands/release.md +32 -0
- package/kit/commands/shape.md +33 -0
- package/kit/commands/verify.md +35 -0
- package/kit/constitution.md +79 -0
- package/kit/orchestrator.md +63 -0
- package/kit/policy-rules/admob.md +25 -0
- package/kit/policy-rules/apple-app-store.md +31 -0
- package/kit/policy-rules/google-play.md +33 -0
- package/kit/policy-rules/privacy.md +25 -0
- package/kit/profiles/android.md +31 -0
- package/kit/profiles/flutter.md +45 -0
- package/kit/profiles/ios.md +32 -0
- package/kit/scripts/_common.sh +24 -0
- package/kit/scripts/analyze.sh +6 -0
- package/kit/scripts/build-android.sh +12 -0
- package/kit/scripts/build-ios.sh +23 -0
- package/kit/scripts/capture-screenshots.sh +20 -0
- package/kit/scripts/compare-goldens.sh +21 -0
- package/kit/scripts/extract-dependencies.sh +23 -0
- package/kit/scripts/extract-permissions.sh +22 -0
- package/kit/scripts/format.sh +11 -0
- package/kit/scripts/scan-secrets.sh +15 -0
- package/kit/scripts/test.sh +12 -0
- package/kit/scripts/validate-ad-ids.sh +22 -0
- package/kit/scripts/validate-release.sh +30 -0
- package/kit/skills/admob-best-practices/SKILL.md +74 -0
- package/kit/skills/mobile-app-development/SKILL.md +69 -0
- package/kit/skills/mobile-privacy-and-permissions/SKILL.md +71 -0
- package/kit/skills/mobile-store-release/SKILL.md +74 -0
- package/kit/skills/mobile-testing-and-visual-qa/SKILL.md +79 -0
- package/kit/skills/small-app-product-design/SKILL.md +68 -0
- package/kit/templates/app-spec.template.md +89 -0
- package/kit/templates/idea.template.md +60 -0
- package/kit/templates/privacy-policy.template.md +41 -0
- package/kit/templates/release-manifest.template.yaml +46 -0
- package/kit/templates/store-listing.template.md +37 -0
- package/kit/templates/tasks.template.md +46 -0
- package/kit/templates/verification-report.template.md +58 -0
- package/package.json +43 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Profile: Flutter (default)
|
|
2
|
+
|
|
3
|
+
The default cross-platform implementation profile. Optimized for small, local-first apps.
|
|
4
|
+
|
|
5
|
+
## Defaults
|
|
6
|
+
- **Framework**: Flutter (stable channel), Dart 3.
|
|
7
|
+
- **State management**: start with `setState` / `ValueNotifier`. Introduce `provider` or
|
|
8
|
+
`riverpod` only when shared state across screens makes it worthwhile. No BLoC for small apps.
|
|
9
|
+
- **Local storage**: `shared_preferences` for small key/value; `sqflite` or `drift` only when
|
|
10
|
+
there is a real relational/list dataset. Hide it behind a repository interface.
|
|
11
|
+
- **Navigation**: `Navigator 2` via `go_router` if there are 3+ routes, otherwise plain
|
|
12
|
+
`Navigator`. Portrait-only unless content needs landscape.
|
|
13
|
+
- **Ads**: `google_mobile_ads`, isolated behind an `AdService`/adapter. Test ad unit ids in
|
|
14
|
+
debug, real ids only via `--dart-define` in release.
|
|
15
|
+
- **Config**: environment via `--dart-define` (`APP_ENV=dev|prod`); never hardcode prod ids.
|
|
16
|
+
- **DI**: constructor injection. Avoid `get_it` unless the graph is genuinely large.
|
|
17
|
+
|
|
18
|
+
## Minimum OS targets
|
|
19
|
+
- **Android**: `minSdk 24` (Android 7.0), `targetSdk 35` / `compileSdk 35` (required by Google
|
|
20
|
+
Play for new apps since Aug 31 2025).
|
|
21
|
+
- **iOS**: deployment target `iOS 15.0`.
|
|
22
|
+
|
|
23
|
+
## Suggested structure
|
|
24
|
+
```
|
|
25
|
+
lib/
|
|
26
|
+
├── app/ app.dart, routes.dart, theme.dart
|
|
27
|
+
├── core/ config/, ads/, storage/, utilities/
|
|
28
|
+
├── features/ home/, settings/, <core_feature>/
|
|
29
|
+
└── main.dart
|
|
30
|
+
test/ unit/, widgets/, helpers/
|
|
31
|
+
integration_test/ critical_flow_test.dart, screenshot_flow_test.dart
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quality commands
|
|
35
|
+
- Format: `dart format .`
|
|
36
|
+
- Analyze: `flutter analyze`
|
|
37
|
+
- Test: `flutter test --coverage`
|
|
38
|
+
- Build Android: `flutter build appbundle --release`
|
|
39
|
+
- Build iOS: `flutter build ipa --release` (or `flutter build ios --release --no-codesign` on CI)
|
|
40
|
+
- Integration: `flutter test integration_test/`
|
|
41
|
+
|
|
42
|
+
## Testability rules
|
|
43
|
+
- Keep business logic out of widgets so it is unit-testable.
|
|
44
|
+
- Provide deterministic seed data for screenshot/integration runs (fixed clock, fixed ids).
|
|
45
|
+
- Every screen state listed in `app-spec.md` must be reachable by a widget or integration test.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Profile: iOS targeting
|
|
2
|
+
|
|
3
|
+
Applies on top of the Flutter profile for the iOS build.
|
|
4
|
+
|
|
5
|
+
## Build configuration
|
|
6
|
+
- Deployment target: iOS 15.0.
|
|
7
|
+
- Bundle identifier: reverse-DNS, must match App Store Connect (e.g. `com.example.timer`).
|
|
8
|
+
- `CFBundleShortVersionString` (marketing version) and `CFBundleVersion` (build, increasing).
|
|
9
|
+
- Build with the current Xcode / latest iOS SDK (Apple requires builds against a recent SDK).
|
|
10
|
+
|
|
11
|
+
## Privacy (Info.plist)
|
|
12
|
+
- Every accessed resource needs a clear `*UsageDescription` string explaining why
|
|
13
|
+
(`NSCameraUsageDescription`, `NSLocationWhenInUseUsageDescription`, etc.). Missing or vague
|
|
14
|
+
strings are a top rejection cause (Guideline 5.1.1).
|
|
15
|
+
- **Privacy manifest** (`PrivacyInfo.xcprivacy`): declare collected data types, tracking, and
|
|
16
|
+
reasons for required-reason APIs. Third-party SDKs (incl. Google Mobile Ads) must ship their
|
|
17
|
+
own privacy manifests and signatures — keep them updated.
|
|
18
|
+
- **App Tracking Transparency**: if any SDK tracks across apps/sites, add
|
|
19
|
+
`NSUserTrackingUsageDescription` and request authorization before tracking.
|
|
20
|
+
|
|
21
|
+
## Signing & release
|
|
22
|
+
- Requires an Apple Developer Program account ($99/yr) — human owns this.
|
|
23
|
+
- Certificates + provisioning profiles via Xcode automatic signing or App Store Connect API.
|
|
24
|
+
- Output: `.ipa` via `flutter build ipa --release`, then upload via Xcode/Transporter.
|
|
25
|
+
- Use **TestFlight** for review-equivalent testing before App Store submission.
|
|
26
|
+
|
|
27
|
+
## Store essentials
|
|
28
|
+
- App Privacy "nutrition label" (must match the privacy manifest and real behavior),
|
|
29
|
+
age rating, App Store screenshots per required device sizes, support URL, reviewer notes,
|
|
30
|
+
export-compliance answer (usually "no" for standard HTTPS / no custom crypto).
|
|
31
|
+
- AdMob: set `GADApplicationIdentifier` in `Info.plist` (real id only in release) and add
|
|
32
|
+
`SKAdNetworkItems` provided by AdMob for attribution.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared helpers for AppKit scripts. Source this from each script.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
APPKIT_DIR="${APPKIT_DIR:-.appkit}"
|
|
6
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
|
|
7
|
+
|
|
8
|
+
c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
|
9
|
+
c_grn() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|
10
|
+
c_ylw() { printf '\033[33m%s\033[0m\n' "$*"; }
|
|
11
|
+
info() { printf '• %s\n' "$*"; }
|
|
12
|
+
ok() { c_grn "✓ $*"; }
|
|
13
|
+
warn() { c_ylw "! $*"; }
|
|
14
|
+
fail() { c_red "✗ $*"; }
|
|
15
|
+
|
|
16
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
17
|
+
|
|
18
|
+
# Skip cleanly (exit 0) when a required tool is missing, recording a gap rather than failing.
|
|
19
|
+
need() {
|
|
20
|
+
if ! have "$1"; then
|
|
21
|
+
warn "SKIP: '$1' not found — recording an explicit verification gap (not a pass)."
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the Android release App Bundle. Production ad ids/env passed via --dart-define.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
need flutter
|
|
5
|
+
OUT="$APPKIT_DIR/release/builds/android"; mkdir -p "$OUT"
|
|
6
|
+
info "Building release .aab (APP_ENV=prod)…"
|
|
7
|
+
if flutter build appbundle --release --dart-define=APP_ENV=prod; then
|
|
8
|
+
AAB=$(ls -t build/app/outputs/bundle/release/*.aab 2>/dev/null | head -1)
|
|
9
|
+
if [ -n "${AAB:-}" ]; then cp "$AAB" "$OUT/" && ok "AAB -> $OUT/$(basename "$AAB")"; else warn "Build ok but .aab not found in default path"; fi
|
|
10
|
+
else
|
|
11
|
+
fail "Android release build failed"; exit 1
|
|
12
|
+
fi
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the iOS release. Requires macOS + Xcode. Without signing, build the app for archive prep.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
need flutter
|
|
5
|
+
OUT="$APPKIT_DIR/release/builds/ios"; mkdir -p "$OUT"
|
|
6
|
+
if [ "$(uname)" != "Darwin" ]; then
|
|
7
|
+
warn "iOS builds require macOS + Xcode. Writing archive instructions instead."
|
|
8
|
+
cat > "$OUT/ARCHIVE_INSTRUCTIONS.md" <<'EOF'
|
|
9
|
+
# iOS Archive (run on macOS with Xcode)
|
|
10
|
+
1. flutter build ipa --release --dart-define=APP_ENV=prod
|
|
11
|
+
2. Or: open ios/Runner.xcworkspace, set Team & signing, Product > Archive.
|
|
12
|
+
3. Validate the archive, then upload via Xcode Organizer / Transporter / App Store Connect API.
|
|
13
|
+
EOF
|
|
14
|
+
ok "Wrote $OUT/ARCHIVE_INSTRUCTIONS.md"; exit 0
|
|
15
|
+
fi
|
|
16
|
+
info "Building iOS release archive (.ipa)…"
|
|
17
|
+
if flutter build ipa --release --dart-define=APP_ENV=prod; then
|
|
18
|
+
IPA=$(ls -t build/ios/ipa/*.ipa 2>/dev/null | head -1)
|
|
19
|
+
[ -n "${IPA:-}" ] && cp "$IPA" "$OUT/" && ok "IPA -> $OUT/$(basename "$IPA")" || warn "Build ok; archive at build/ios/archive"
|
|
20
|
+
else
|
|
21
|
+
warn "Signed IPA build failed (likely signing). Try: flutter build ios --release --no-codesign"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Capture screenshot catalog via the integration_test driver into .appkit/screenshots/qa/.
|
|
3
|
+
# Requires a running emulator/simulator or device.
|
|
4
|
+
source "$(dirname "$0")/_common.sh"
|
|
5
|
+
need flutter
|
|
6
|
+
QA="$APPKIT_DIR/screenshots/qa"; mkdir -p "$QA"
|
|
7
|
+
DRIVER="integration_test/screenshot_flow_test.dart"
|
|
8
|
+
if [ ! -f "$DRIVER" ]; then
|
|
9
|
+
warn "No $DRIVER yet. Add a screenshot integration test that calls binding.takeScreenshot(id)."
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
if ! flutter devices 2>/dev/null | grep -qiE 'emulator|simulator|device'; then
|
|
13
|
+
warn "No device/emulator detected — recording a screenshot gap (not a pass)."; exit 0
|
|
14
|
+
fi
|
|
15
|
+
info "Capturing screenshots (APP_ENV=dev, seeded data)…"
|
|
16
|
+
if flutter test "$DRIVER" --dart-define=APP_ENV=dev --dart-define=APPKIT_SCREENSHOTS=1; then
|
|
17
|
+
ok "Screenshots captured to $QA (test must write PNGs there)"
|
|
18
|
+
else
|
|
19
|
+
fail "Screenshot run failed"; exit 1
|
|
20
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Compare captured QA screenshots against approved baselines (pixel-diff).
|
|
3
|
+
# Establish baselines by copying qa/ -> baselines/ once approved.
|
|
4
|
+
source "$(dirname "$0")/_common.sh"
|
|
5
|
+
QA="$APPKIT_DIR/screenshots/qa"; BASE="$APPKIT_DIR/screenshots/baselines"
|
|
6
|
+
mkdir -p "$BASE"
|
|
7
|
+
[ -d "$QA" ] || { warn "No QA screenshots yet."; exit 0; }
|
|
8
|
+
diffs=0; missing=0
|
|
9
|
+
for img in "$QA"/*.png; do
|
|
10
|
+
[ -e "$img" ] || { warn "No QA PNGs found."; exit 0; }
|
|
11
|
+
name=$(basename "$img"); b="$BASE/$name"
|
|
12
|
+
if [ ! -f "$b" ]; then warn "No baseline for $name (new state)"; missing=$((missing+1)); continue; fi
|
|
13
|
+
if have compare; then
|
|
14
|
+
d=$(compare -metric AE "$img" "$b" null: 2>&1 || true)
|
|
15
|
+
if [ "${d%% *}" != "0" ]; then fail "$name differs (AE=$d)"; diffs=$((diffs+1)); else ok "$name matches"; fi
|
|
16
|
+
else
|
|
17
|
+
if cmp -s "$img" "$b"; then ok "$name matches (byte)"; else fail "$name differs (byte)"; diffs=$((diffs+1)); fi
|
|
18
|
+
fi
|
|
19
|
+
done
|
|
20
|
+
info "Golden compare: $diffs diffs, $missing without baseline."
|
|
21
|
+
[ "$diffs" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# List direct dependencies and flag known SDK categories for the privacy/SDK review.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
[ -f pubspec.yaml ] || { warn "No pubspec.yaml found."; exit 0; }
|
|
5
|
+
echo "== Direct dependencies (pubspec.yaml) =="
|
|
6
|
+
awk '/^dependencies:/{f=1;next} /^dev_dependencies:|^[a-z]/{if(f && $0 !~ /^[[:space:]]/) f=0} f && /^[[:space:]]+[a-z]/{print " "$1}' pubspec.yaml | sed 's/://'
|
|
7
|
+
echo
|
|
8
|
+
echo "== Category flags =="
|
|
9
|
+
flag() { grep -qiE "(^|[^a-z])$1" pubspec.yaml && echo " [$2] $1"; }
|
|
10
|
+
flag google_mobile_ads ADS
|
|
11
|
+
flag firebase_analytics ANALYTICS
|
|
12
|
+
flag firebase_crashlytics CRASH
|
|
13
|
+
flag sentry CRASH
|
|
14
|
+
flag google_sign_in AUTH
|
|
15
|
+
flag firebase_auth AUTH
|
|
16
|
+
flag http NETWORK
|
|
17
|
+
flag dio NETWORK
|
|
18
|
+
flag shared_preferences STORAGE
|
|
19
|
+
flag sqflite STORAGE
|
|
20
|
+
flag drift STORAGE
|
|
21
|
+
echo
|
|
22
|
+
info "Cross-check this inventory against the privacy declarations (mobile-privacy-and-permissions)."
|
|
23
|
+
[ -n "$(command -v flutter)" ] && info "For the full tree run: flutter pub deps --style=compact"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Extract declared permissions from Android manifests and iOS Info.plist usage strings.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
echo "== Android permissions =="
|
|
5
|
+
found=0
|
|
6
|
+
while IFS= read -r -d '' m; do
|
|
7
|
+
echo "-- $m"
|
|
8
|
+
grep -oE 'android:name="[^"]+"' "$m" | grep -i permission | sed 's/android:name=//; s/"//g' | sort -u
|
|
9
|
+
found=1
|
|
10
|
+
done < <(find android -name AndroidManifest.xml -print0 2>/dev/null)
|
|
11
|
+
[ "$found" = 1 ] || warn "No AndroidManifest.xml found."
|
|
12
|
+
echo
|
|
13
|
+
echo "== iOS usage-description keys (Info.plist) =="
|
|
14
|
+
PLIST="ios/Runner/Info.plist"
|
|
15
|
+
if [ -f "$PLIST" ]; then
|
|
16
|
+
grep -oE 'NS[A-Za-z]+UsageDescription' "$PLIST" | sort -u || warn "No usage-description keys."
|
|
17
|
+
else
|
|
18
|
+
warn "No ios/Runner/Info.plist found."
|
|
19
|
+
fi
|
|
20
|
+
echo
|
|
21
|
+
info "Map each permission to feature → user benefit → runtime text → store declaration in app-spec.md."
|
|
22
|
+
info "Unjustified or high-risk permissions are release blockers (see policy-rules/privacy.md)."
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Format Dart/Flutter sources. CI mode (--check) fails if formatting is needed.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
need dart
|
|
5
|
+
if [ "${1:-}" = "--check" ]; then
|
|
6
|
+
info "Checking formatting (dart format --set-exit-if-changed)…"
|
|
7
|
+
dart format --output=none --set-exit-if-changed . && ok "Formatting clean" || { fail "Needs formatting"; exit 1; }
|
|
8
|
+
else
|
|
9
|
+
info "Formatting (dart format .)…"
|
|
10
|
+
dart format . && ok "Formatted"
|
|
11
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Heuristic secret scan over source + config. Flags likely keys; not a substitute for a vault.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
PATTERNS='(AIza[0-9A-Za-z_-]{30,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|sk_live_[0-9A-Za-z]+|ghp_[0-9A-Za-z]{30,}|aws_secret_access_key|password\s*=\s*["'\''][^"'\'' ]{6,})'
|
|
5
|
+
SCAN_DIRS="lib android/app ios/Runner"
|
|
6
|
+
hits=0
|
|
7
|
+
for d in $SCAN_DIRS; do
|
|
8
|
+
[ -e "$d" ] || continue
|
|
9
|
+
while IFS= read -r line; do echo "$line"; hits=$((hits+1)); done < <(grep -rInE "$PATTERNS" "$d" \
|
|
10
|
+
--include='*.dart' --include='*.gradle' --include='*.kts' --include='*.plist' \
|
|
11
|
+
--include='*.properties' --include='*.xml' --include='*.json' 2>/dev/null)
|
|
12
|
+
done
|
|
13
|
+
# Keystores / key.properties must never be committed.
|
|
14
|
+
find android -name '*.jks' -o -name '*.keystore' 2>/dev/null | while read -r k; do fail "Keystore in tree: $k"; hits=1; done
|
|
15
|
+
if [ "$hits" -eq 0 ]; then ok "No obvious secrets found"; else fail "$hits potential secret(s) — review above"; exit 1; fi
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run tests with coverage. Pass a path to scope (e.g. test/unit) for targeted runs.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
need flutter
|
|
5
|
+
TARGET="${1:-}"
|
|
6
|
+
info "Running flutter test ${TARGET:-(all)} --coverage…"
|
|
7
|
+
if flutter test --coverage $TARGET; then
|
|
8
|
+
ok "Tests passed"
|
|
9
|
+
[ -f coverage/lcov.info ] && info "Coverage: coverage/lcov.info"
|
|
10
|
+
else
|
|
11
|
+
fail "Tests failed"; exit 1
|
|
12
|
+
fi
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Ensure test ad ids aren't shipped in release and prod ids aren't hardcoded in source.
|
|
3
|
+
# Google sample/test ad unit ids contain 3940256099942544.
|
|
4
|
+
source "$(dirname "$0")/_common.sh"
|
|
5
|
+
TEST_ID="3940256099942544"
|
|
6
|
+
issues=0
|
|
7
|
+
# Test ids must not appear outside test/dev-guarded code in a way that ships to release.
|
|
8
|
+
hits=$(grep -rInE "ca-app-pub-${TEST_ID}" lib 2>/dev/null || true)
|
|
9
|
+
if [ -n "$hits" ]; then
|
|
10
|
+
warn "Test ad unit id referenced in lib/ — ensure it's only used when !AppConfig.isProduction:"
|
|
11
|
+
echo "$hits"
|
|
12
|
+
fi
|
|
13
|
+
# Real ad ids should come from env/config, not be hardcoded as production constants.
|
|
14
|
+
prod=$(grep -rInE 'ca-app-pub-[0-9]{16}~[0-9]+|ca-app-pub-[0-9]{16}/[0-9]+' lib 2>/dev/null | grep -v "$TEST_ID" || true)
|
|
15
|
+
if [ -n "$prod" ]; then
|
|
16
|
+
fail "Hardcoded production-looking ad id in lib/ — source from environment configuration:"
|
|
17
|
+
echo "$prod"; issues=$((issues+1))
|
|
18
|
+
fi
|
|
19
|
+
# AdMob App ID present in platform config?
|
|
20
|
+
grep -rq "com.google.android.gms.ads.APPLICATION_ID" android 2>/dev/null && ok "Android AdMob App ID meta-data present" || warn "Android AdMob App ID meta-data not found (ok if no ads)"
|
|
21
|
+
grep -rq "GADApplicationIdentifier" ios 2>/dev/null && ok "iOS GADApplicationIdentifier present" || warn "iOS GADApplicationIdentifier not found (ok if no ads)"
|
|
22
|
+
[ "$issues" -eq 0 ] && ok "Ad id check passed" || { fail "Ad id issues found"; exit 1; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Static release-readiness checks: versions, ids, target API, env, debug leftovers.
|
|
3
|
+
source "$(dirname "$0")/_common.sh"
|
|
4
|
+
issues=0
|
|
5
|
+
note_fail(){ fail "$1"; issues=$((issues+1)); }
|
|
6
|
+
|
|
7
|
+
# Version present in pubspec
|
|
8
|
+
if [ -f pubspec.yaml ]; then
|
|
9
|
+
v=$(grep -E '^version:' pubspec.yaml | awk '{print $2}')
|
|
10
|
+
[ -n "$v" ] && ok "pubspec version: $v" || note_fail "No version: in pubspec.yaml"
|
|
11
|
+
else note_fail "No pubspec.yaml"; fi
|
|
12
|
+
|
|
13
|
+
# Android target/compile SDK 35
|
|
14
|
+
GR="android/app/build.gradle"; GRK="android/app/build.gradle.kts"
|
|
15
|
+
gf=""; [ -f "$GR" ] && gf="$GR"; [ -f "$GRK" ] && gf="$GRK"
|
|
16
|
+
if [ -n "$gf" ]; then
|
|
17
|
+
ts=$(grep -oE 'targetSdk(Version)?[ =]+[0-9]+' "$gf" | grep -oE '[0-9]+' | head -1)
|
|
18
|
+
if [ -n "$ts" ] && [ "$ts" -ge 35 ]; then ok "Android targetSdk=$ts (>=35)"; else note_fail "Android targetSdk=$ts (<35; Play requires 35)"; fi
|
|
19
|
+
else warn "No android build.gradle found."; fi
|
|
20
|
+
|
|
21
|
+
# Debug / placeholder leftovers in lib
|
|
22
|
+
if grep -rInE '\b(TODO|FIXME|Lorem ipsum|placeholder)\b' lib >/dev/null 2>&1; then warn "Placeholder/TODO text in lib/ — review before release."; fi
|
|
23
|
+
if grep -rInE '\bprint\(' lib >/dev/null 2>&1; then warn "print() calls in lib/ — prefer no debug logging in release."; fi
|
|
24
|
+
|
|
25
|
+
# Run the ad-id and secret checks
|
|
26
|
+
bash "$(dirname "$0")/validate-ad-ids.sh" || issues=$((issues+1))
|
|
27
|
+
bash "$(dirname "$0")/scan-secrets.sh" || issues=$((issues+1))
|
|
28
|
+
|
|
29
|
+
echo
|
|
30
|
+
if [ "$issues" -eq 0 ]; then ok "Static release validation passed (still run a real release smoke test on device)."; else fail "$issues release issue(s) — see above."; exit 1; fi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: admob-best-practices
|
|
3
|
+
description: Plan and integrate safe, policy-compliant AdMob monetization. Use during Shape, Build and Verify for ad-format selection, banner/interstitial/rewarded/app-open placement rules, frequency and natural-transition rules, accidental-click prevention, test vs production ad ids, consent (UMP/GDPR), child-directed settings, failure/offline behavior and the AdMob release checklist.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AdMob Best Practices
|
|
7
|
+
|
|
8
|
+
Monetize without harming usability or violating policy. Ads are part of UX, decided in Shape.
|
|
9
|
+
|
|
10
|
+
## Format selection (default preference order)
|
|
11
|
+
1. **Anchored adaptive banner** — safe default, low risk, persistent.
|
|
12
|
+
2. **Rewarded** — only user-initiated, with a clear value exchange.
|
|
13
|
+
3. **Interstitial** — only at natural transitions (level end, save, screen completion).
|
|
14
|
+
4. **App-open** — only on cold start / return to foreground, infrequently.
|
|
15
|
+
5. **Native** — only when the app naturally has a feed; needs careful labeling.
|
|
16
|
+
|
|
17
|
+
Avoid advanced formats unless the app naturally supports them. Many small utilities should
|
|
18
|
+
ship **banner-only** or no ads.
|
|
19
|
+
|
|
20
|
+
## Placement spec (every placement, in app-spec.md)
|
|
21
|
+
```yaml
|
|
22
|
+
placement_id: home_bottom_banner
|
|
23
|
+
format: anchored_adaptive_banner
|
|
24
|
+
screen: HOME
|
|
25
|
+
trigger: screen_visible
|
|
26
|
+
frequency: sdk_managed
|
|
27
|
+
critical_interaction_conflict: false
|
|
28
|
+
accidental_click_risk: low
|
|
29
|
+
loading_behavior: preserve_layout
|
|
30
|
+
failure_behavior: collapse_or_preserve_clean_space
|
|
31
|
+
development_ad_id: test
|
|
32
|
+
production_ad_id_source: environment_configuration
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Hard policy rules (AdMob / Google Play / Apple)
|
|
36
|
+
- **Never** place ads where they obstruct content or sit next to interactive controls
|
|
37
|
+
(accidental clicks = policy violation and account risk).
|
|
38
|
+
- **No** ads that simulate app UI, "X" buttons that lead to clicks, or unexpected fullscreen
|
|
39
|
+
ads. Interstitials must not appear on app launch or mid-action.
|
|
40
|
+
- **Rewarded** ads must be explicitly user-initiated and clearly described before play.
|
|
41
|
+
- **App-open** ads must not be shown aggressively or block the user repeatedly.
|
|
42
|
+
- Do not click your own ads or encourage clicks. Do not place ads on screens with no content.
|
|
43
|
+
- Maintain reasonable ad density; one banner per screen is plenty for small apps.
|
|
44
|
+
|
|
45
|
+
## Test vs production ids
|
|
46
|
+
- Use Google's **sample/test ad unit ids** (and register test devices) during development.
|
|
47
|
+
- Real ad unit ids only in release via environment config. Hardcoded production ids in debug
|
|
48
|
+
builds or test ids in release are **critical** issues.
|
|
49
|
+
- The AdMob **App ID** goes in `AndroidManifest.xml` and `Info.plist`; use the real id only in
|
|
50
|
+
release.
|
|
51
|
+
|
|
52
|
+
## Consent & privacy
|
|
53
|
+
- Implement Google **UMP / User Messaging Platform** for GDPR/EEA + UK consent and the CCPA
|
|
54
|
+
signal where required. Gate personalized ads on consent; fall back to non-personalized ads.
|
|
55
|
+
- Reflect the advertising id / tracking in Data Safety, App Privacy, and the privacy manifest
|
|
56
|
+
(see `mobile-privacy-and-permissions`).
|
|
57
|
+
- iOS: personalized ads that track require ATT authorization. Non-personalized ads avoid this.
|
|
58
|
+
|
|
59
|
+
## Child-directed
|
|
60
|
+
If the app targets children, set `tagForChildDirectedTreatment` / `tagForUnderAgeOfConsent`,
|
|
61
|
+
serve only family-safe, non-personalized ads (or no ads), and confirm with the human first.
|
|
62
|
+
|
|
63
|
+
## Failure & offline behavior
|
|
64
|
+
- Ad load failure must degrade cleanly: collapse the slot or keep clean reserved space; never
|
|
65
|
+
block content or show error UI. The app must be fully usable offline with no ads.
|
|
66
|
+
|
|
67
|
+
## Verify checklist
|
|
68
|
+
- [ ] Test ids in dev, real ids only in release (scripted check).
|
|
69
|
+
- [ ] App ID set per platform, env-gated.
|
|
70
|
+
- [ ] Each placement matches `app-spec.md`; no obstruction / no risky adjacency.
|
|
71
|
+
- [ ] Interstitials only at natural transitions; rewarded user-initiated; app-open not aggressive.
|
|
72
|
+
- [ ] Failure & offline paths preserve usability.
|
|
73
|
+
- [ ] Consent (UMP) wired; child-directed settings correct where relevant.
|
|
74
|
+
- [ ] No hardcoded secrets.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-app-development
|
|
3
|
+
description: Implement small local-first Flutter mobile apps. Use during the Build and Fix phases for project setup, feature-oriented structure, state management, navigation, local storage, environment config, the AdMob adapter, dependency hygiene, release configuration and writing testable code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mobile App Development
|
|
7
|
+
|
|
8
|
+
Implementation guidance for the default Flutter profile. Keep architecture **proportional**
|
|
9
|
+
to a small app — the goal is a shippable MVP, not a framework.
|
|
10
|
+
|
|
11
|
+
## Project setup
|
|
12
|
+
- `flutter create --org com.example --platforms=android,ios <name>`.
|
|
13
|
+
- Set application id / bundle id immediately and consistently (they are hard to change later).
|
|
14
|
+
- Pin the Flutter/Dart SDK constraint in `pubspec.yaml`. Commit `pubspec.lock`.
|
|
15
|
+
- Configure `analysis_options.yaml` with `flutter_lints`.
|
|
16
|
+
|
|
17
|
+
## Structure (feature-oriented)
|
|
18
|
+
```
|
|
19
|
+
lib/
|
|
20
|
+
├── app/ app.dart (MaterialApp), routes.dart, theme.dart
|
|
21
|
+
├── core/ config/ (env), ads/ (AdService), storage/ (repository), utilities/
|
|
22
|
+
├── features/ <feature>/ -> screen widgets + a controller/notifier + models
|
|
23
|
+
└── main.dart
|
|
24
|
+
```
|
|
25
|
+
- Separate **UI / state / storage**. Widgets render; controllers hold logic; repositories
|
|
26
|
+
persist. This keeps logic unit-testable and storage swappable.
|
|
27
|
+
|
|
28
|
+
## State management
|
|
29
|
+
- Default to `setState`/`ValueNotifier`/`ChangeNotifier`. Reach for `provider`/`riverpod`
|
|
30
|
+
only when state is shared across screens. No global mutable state unless justified.
|
|
31
|
+
|
|
32
|
+
## Local storage
|
|
33
|
+
- Hide persistence behind a `Repository` interface; provide an in-memory fake for tests.
|
|
34
|
+
- `shared_preferences` for small key/value; `drift`/`sqflite` for lists/relations.
|
|
35
|
+
- Support a trivial **migration** hook (a stored schema version) even in v1.
|
|
36
|
+
|
|
37
|
+
## Navigation & UX
|
|
38
|
+
- Portrait by default. Use `go_router` at 3+ routes.
|
|
39
|
+
- Handle empty / loading / error states for every screen — they are spec'd and screenshotted.
|
|
40
|
+
- Keyboard-aware: wrap forms in scroll views and respect `MediaQuery.viewInsets` so primary
|
|
41
|
+
actions never hide behind the keyboard.
|
|
42
|
+
|
|
43
|
+
## Environment configuration
|
|
44
|
+
- `APP_ENV` via `--dart-define`. A `core/config/AppConfig` exposes `isProduction`, ad ids, etc.
|
|
45
|
+
- **Never** hardcode production ad ids or endpoints. Production values come from
|
|
46
|
+
`--dart-define` at release-build time only.
|
|
47
|
+
|
|
48
|
+
## AdMob adapter
|
|
49
|
+
- One `AdService` wraps `google_mobile_ads`. The rest of the app talks only to `AdService`.
|
|
50
|
+
- It returns **test** ad unit ids unless `AppConfig.isProduction`. Loading failures must
|
|
51
|
+
return cleanly so the UI degrades gracefully (collapse/preserve space, never block content).
|
|
52
|
+
- See `admob-best-practices` for placement rules.
|
|
53
|
+
|
|
54
|
+
## Dependencies
|
|
55
|
+
- Each new dependency must earn its place. Prefer the platform/std lib. Audit every package's
|
|
56
|
+
permissions, SDKs, and privacy impact before adding it (see `mobile-privacy-and-permissions`).
|
|
57
|
+
|
|
58
|
+
## Release configuration hygiene
|
|
59
|
+
- No debug menus, `print`/log spam, placeholder text, or test ad ids in release builds.
|
|
60
|
+
- Correct application id, version, and `APP_ENV=prod`. Enable code shrinking on Android.
|
|
61
|
+
|
|
62
|
+
## Change control
|
|
63
|
+
Do not silently change product objective, target user, monetization, data collection,
|
|
64
|
+
permissions, backend usage, store target, or acceptance criteria. If implementation forces a
|
|
65
|
+
change, update `app-spec.md` and note the reason (constitution §9).
|
|
66
|
+
|
|
67
|
+
## Testability
|
|
68
|
+
Write tests alongside code (see `mobile-testing-and-visual-qa`). If something is hard to test,
|
|
69
|
+
it is usually badly factored — move logic out of the widget.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-privacy-and-permissions
|
|
3
|
+
description: Ensure minimal data handling and accurate privacy declarations for mobile apps. Use across all phases for data and SDK inventory, personal/sensitive data classification, data minimization, permission justification and runtime messaging, privacy-policy drafting, and Google Data Safety + Apple App Privacy + iOS privacy manifest mapping from real implementation evidence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mobile Privacy & Permissions
|
|
7
|
+
|
|
8
|
+
Privacy is a release gate. Declarations must match what the code actually does.
|
|
9
|
+
|
|
10
|
+
## Data inventory (build from evidence, not intent)
|
|
11
|
+
For each piece of data the app touches, record:
|
|
12
|
+
`name · type · source · stored where (local/remote) · transmitted? · purpose · retention ·
|
|
13
|
+
deletion · linked to user identity? · used for tracking?`
|
|
14
|
+
|
|
15
|
+
Local-first apps should be able to answer: "all data stays on device, nothing transmitted".
|
|
16
|
+
If a third-party SDK breaks that, it must appear in the inventory.
|
|
17
|
+
|
|
18
|
+
## SDK inventory
|
|
19
|
+
List direct + relevant transitive dependencies and classify each: advertising, analytics,
|
|
20
|
+
crash-reporting, auth, storage, network. For each, note what data it collects and whether it
|
|
21
|
+
ships a privacy manifest (iOS). The classic small-app footprint is just **Google Mobile Ads**.
|
|
22
|
+
|
|
23
|
+
## Data classification
|
|
24
|
+
- **Personal**: name, email, precise location, device identifiers (incl. advertising id),
|
|
25
|
+
contacts, photos.
|
|
26
|
+
- **Sensitive**: health, finance, sexual orientation, religion, precise location, children's
|
|
27
|
+
data, biometrics. Any sensitive data triggers human confirmation (constitution §8).
|
|
28
|
+
|
|
29
|
+
## Minimization
|
|
30
|
+
Collect nothing you do not use. Prefer on-device processing. No analytics unless justified and
|
|
31
|
+
declared. No advertising id if ads can run non-personalized.
|
|
32
|
+
|
|
33
|
+
## Permissions — justification map
|
|
34
|
+
Each requested permission must map to: requirement → feature → user benefit → runtime
|
|
35
|
+
explanation → store declaration. **An unjustified permission is a release blocker.**
|
|
36
|
+
|
|
37
|
+
High-risk permissions (block unless human-confirmed): fine/background location, contacts, SMS,
|
|
38
|
+
call log, accessibility service, VPN, device admin, all-files/external-storage,
|
|
39
|
+
`QUERY_ALL_PACKAGES`, `REQUEST_INSTALL_PACKAGES`, camera/mic when not core.
|
|
40
|
+
|
|
41
|
+
### Runtime messaging
|
|
42
|
+
- Android: request at point of use with rationale; respect denial.
|
|
43
|
+
- iOS: every accessed resource needs a clear `*UsageDescription` in `Info.plist`. Vague strings
|
|
44
|
+
are a top rejection cause (Guideline 5.1.1).
|
|
45
|
+
|
|
46
|
+
## Google Play Data Safety mapping
|
|
47
|
+
Fill the Data Safety form from the data inventory: data types collected/shared, whether linked
|
|
48
|
+
to identity, whether used for tracking, encryption in transit, deletion request mechanism.
|
|
49
|
+
Must match the in-app behavior and the privacy policy. Note the **advertising id** handling
|
|
50
|
+
and declare the `AD_ID` permission consistently.
|
|
51
|
+
|
|
52
|
+
## Apple App Privacy + privacy manifest mapping
|
|
53
|
+
- App Privacy "nutrition label" in App Store Connect: data types, linkage, tracking — must
|
|
54
|
+
match real behavior and the privacy manifest.
|
|
55
|
+
- `PrivacyInfo.xcprivacy`: declare collected data types, tracking flag, and **required-reason
|
|
56
|
+
API** usage. Bundled SDKs (incl. Google Mobile Ads) must provide their own manifests +
|
|
57
|
+
signatures; keep them current or the build is rejected.
|
|
58
|
+
- **App Tracking Transparency**: if any SDK tracks across apps/sites, request authorization via
|
|
59
|
+
ATT and add `NSUserTrackingUsageDescription`. If you only show non-personalized ads, you can
|
|
60
|
+
avoid tracking and say so consistently.
|
|
61
|
+
|
|
62
|
+
## Privacy policy
|
|
63
|
+
Even a local-only app usually needs a hosted privacy policy URL (both stores require one when
|
|
64
|
+
ads/SDKs are present). Draft from the data + SDK inventory. Use the template in
|
|
65
|
+
`templates/privacy-policy.template.md`. Mark every placeholder (`<HOSTED_URL>`) clearly; never
|
|
66
|
+
invent legal commitments — flag them for human review.
|
|
67
|
+
|
|
68
|
+
## Children
|
|
69
|
+
If the audience includes children: Google Play Families policy and Apple Kids Category apply,
|
|
70
|
+
ads must use child-directed/`tagForChildDirectedTreatment` settings, no behavioral tracking,
|
|
71
|
+
and COPPA/GDPR-K obligations attach. Always escalate to the human first.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-store-release
|
|
3
|
+
description: Guide store review, signing, metadata, screenshots, declarations, release preparation and rejection handling for Google Play and the Apple App Store. Use during Verify and Release for policy review, Android/iOS signing, versioning, store metadata, content rating, reviewer notes, test tracks/TestFlight, release checklists and store-rejection classification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mobile Store Release
|
|
7
|
+
|
|
8
|
+
Prepare complete, policy-aware release packages. The kit prepares; the human submits.
|
|
9
|
+
|
|
10
|
+
## Google Play review (inspect)
|
|
11
|
+
- **Functionality / minimum-functionality**: app must do something useful and complete — no
|
|
12
|
+
placeholder/"coming soon", no webview-only wrappers.
|
|
13
|
+
- **Target API**: new apps & updates must target **API 35 (Android 15)** since Aug 31 2025.
|
|
14
|
+
- **Permissions**: every permission justified; sensitive permissions need declarations.
|
|
15
|
+
- **Data Safety** form consistent with real behavior + privacy policy; declare advertising id.
|
|
16
|
+
- **Ads** declaration set; ad behavior compliant (see `admob-best-practices`).
|
|
17
|
+
- **Content rating** via IARC questionnaire; **target audience & content** (Families if kids).
|
|
18
|
+
- **Privacy policy URL** required (always when ads/SDKs present).
|
|
19
|
+
- **Account/app access** info if there is a login or gated content.
|
|
20
|
+
- **App signing**: enroll in Play App Signing; upload `.aab`.
|
|
21
|
+
- **Testing track**: new personal developer accounts must run closed testing before production.
|
|
22
|
+
- Correct, immutable **package name**; correct version code/name.
|
|
23
|
+
|
|
24
|
+
## Apple App Store review (inspect)
|
|
25
|
+
- **Guideline 2.1 App Completeness** (top rejection ~40%): no crashes, no bugs, no placeholder
|
|
26
|
+
content, all features functional, demo/login provided if needed.
|
|
27
|
+
- **Guideline 5.1.1 Privacy**: clear permission purpose strings; App Privacy label matches
|
|
28
|
+
behavior; privacy manifest present; ATT if tracking.
|
|
29
|
+
- **Stability**: must not crash on the reviewer's device / current iOS.
|
|
30
|
+
- **Business model** clear; ads behave; no hidden/undocumented features.
|
|
31
|
+
- **Metadata** accurate (no misleading screenshots/claims); **reviewer notes** explain
|
|
32
|
+
non-obvious behavior; demo account if applicable.
|
|
33
|
+
- **Export compliance** answer (usually "no" for standard HTTPS).
|
|
34
|
+
- Correct **bundle id**; signing & provisioning ready; build against current SDK.
|
|
35
|
+
- Use **TestFlight** before submission.
|
|
36
|
+
|
|
37
|
+
## Signing (never expose secrets)
|
|
38
|
+
**Android**: create an upload keystore (`keytool`), store it + passwords outside the repo
|
|
39
|
+
(secrets manager / CI secret); configure `signingConfigs` to read from env/`key.properties`
|
|
40
|
+
(git-ignored); enroll in Play App Signing; verify with `apksigner`/`bundletool`.
|
|
41
|
+
**iOS**: own bundle id in App Store Connect; team + certificates + provisioning profile (Xcode
|
|
42
|
+
automatic or ASC API key); archive and validate before upload. The **human owns** signing
|
|
43
|
+
identities and the developer accounts.
|
|
44
|
+
|
|
45
|
+
## Versioning
|
|
46
|
+
- Android: `versionName` (semver, e.g. `1.0.0`) + integer `versionCode` (always increasing).
|
|
47
|
+
- iOS: `CFBundleShortVersionString` + increasing `CFBundleVersion`. Keep them in sync with
|
|
48
|
+
`pubspec.yaml` `version: 1.0.0+1`.
|
|
49
|
+
|
|
50
|
+
## Store metadata
|
|
51
|
+
App name · short/full description · subtitle (iOS) · keywords (iOS) · category · promotional
|
|
52
|
+
text · release notes · privacy-policy/support/marketing URLs (placeholders OK, marked) ·
|
|
53
|
+
reviewer notes. No misleading claims; do not show unavailable features.
|
|
54
|
+
|
|
55
|
+
## Store screenshots
|
|
56
|
+
Separate QA screenshots from marketing screenshots:
|
|
57
|
+
`screenshots/qa/` · `screenshots/store-raw/` · `screenshots/store-final/`.
|
|
58
|
+
Marketing shots reflect the real release build, use realistic safe sample data, match required
|
|
59
|
+
platform dimensions, avoid debug data and misleading content.
|
|
60
|
+
|
|
61
|
+
## Release checklist (per platform)
|
|
62
|
+
- [ ] Verification READY (or approved with warnings), no critical/unresolved-major issues.
|
|
63
|
+
- [ ] Correct app id / bundle id, version, `APP_ENV=prod`, real ad ids only here.
|
|
64
|
+
- [ ] Signing configured, secrets out of repo.
|
|
65
|
+
- [ ] Metadata, screenshots, privacy declarations, reviewer notes prepared.
|
|
66
|
+
- [ ] Privacy policy + support URLs resolved.
|
|
67
|
+
- [ ] Human approval recorded before submission.
|
|
68
|
+
|
|
69
|
+
## Store-rejection classification (for `/appkit.fix rejection`)
|
|
70
|
+
Map the rejection text to a category → requirement → code/metadata fix:
|
|
71
|
+
minimum functionality · crash/bug · privacy/permission · metadata/screenshot · ads ·
|
|
72
|
+
data-safety/privacy-label mismatch · account access · IP/content · signing/build config.
|
|
73
|
+
Prefer the **minimal** change, add a regression test/check, update the affected declaration,
|
|
74
|
+
then re-verify before resubmitting. Document the change for the resubmission notes.
|