Skip to content

feat(cli): expand iOS pre-scan checks (plist, Xcode, entitlements, Capacitor config, pods/SPM, app icons)#2565

Open
WcaleNieWolny wants to merge 7 commits into
mainfrom
wolny/prescan-ios-expansion
Open

feat(cli): expand iOS pre-scan checks (plist, Xcode, entitlements, Capacitor config, pods/SPM, app icons)#2565
WcaleNieWolny wants to merge 7 commits into
mainfrom
wolny/prescan-ios-expansion

Conversation

@WcaleNieWolny

@WcaleNieWolny WcaleNieWolny commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the build pre-scan feature. Expands the iOS pre-scan from credential/profile-only coverage to the full local iOS project surface, so a misconfigured Xcode/Capacitor project is caught before anything is uploaded to a cloud build.

New iOS checks by group

Info.plist / App Store (ios/plist-*)

  • plist-bundle-id-format, plist-version-short-format, plist-version-build-format
  • plist-encryption-compliance, plist-ats-arbitrary-loads, plist-launch-storyboard
  • plist-orientations-present, plist-orientations-multitasking, plist-display-name, plist-background-modes-sanity

Xcode project / build settings (ios/xcode-*)

  • xcode-deployment-target-capacitor, xcode-signing-team, xcode-bundle-id-mismatch-across-configs
  • xcode-enable-bitcode-leftover, xcode-swift-version-sanity, xcode-no-app-target, xcode-multiple-app-targets

Entitlements / capabilities (ios/entitlements-*)

  • entitlements-vs-profile-capability, entitlements-aps-environment-vs-mode
  • entitlements-associated-domains-format, entitlements-app-groups-format

Capacitor config (ios/capacitor-*)

  • capacitor-server-url-shipped, capacitor-server-cleartext, capacitor-allow-navigation-wildcard

Pods / SPM (ios/pods-*, ios/spm-*)

  • pods-not-installed, pods-lock-missing, pods-capacitor-missing
  • spm-package-resolved-missing, spm-capacitor-dependency-missing, spm-deployment-target-consistency

App icons (ios/appicon-*)

  • appicon-empty-or-placeholder, appicon-referenced-file-missing, appicon-marketing-missing

Net +33 registered iOS checks (ALL_CHECKS 46 -> 80).

Parsing helpers

Shared, FP-resistant parsers backing the checks:

  • plist reader/value resolver that substitutes Xcode build-variable references ($(PRODUCT_BUNDLE_IDENTIFIER), $(MARKETING_VERSION), etc.) so build-variable Info.plist values are not flagged as literals
  • pbxproj build-settings reader (per-config), entitlements reader, and an asset-catalog / AppIcon.appiconset reader
  • a shared capacitorMajor helper for Capacitor-version-aware checks

Severity calibration (builder-verified)

Severities were tuned against the real Capacitor 8 builder project to avoid false build-blocks. Notably the Capacitor-default aps-environment: development entitlement on a push-free app surfaces as a warning (not an error) under app_store, so a healthy project still scans clean of errors. Other Capacitor-default-leftover and cosmetic conditions were downgraded from error to warning/info after confirming the builder tolerates them.

Upload-gated checks

Checks whose answer depends on the target distribution mode / upload destination (e.g. APS environment vs app_store / ad_hoc, provisioning capability pairing) are gated on the distribution mode and only run when the relevant context is present, rather than firing unconditionally.

Verified clean on the real project

Scanned the real Capacitor 8 SPM project (capgo_builder/tutorial-app) with --platform ios: 0 errors and 0 warnings from any of the new iOS structural checks. The only findings are pre-existing shared checks (remote apikey/credentials, and one bundle-id-consistency warning) — none from the new plist/Xcode/entitlements/Capacitor/pods-SPM/app-icon pack.

Tests

bun test test/prescan/: 617 pass / 0 fail across 25 files.

https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A

Summary by CodeRabbit

Release Notes

  • New Features

    • Expanded iOS pre-build validation with new checks for Capacitor configuration security (server URL/cleartext/navigation), Info.plist format compliance, Xcode project/build-setting consistency (including signing/deployment/Swift/bitcode), provisioning-profile entitlement capability verification, and AppIcon asset/marketing icon verification.
  • Tests

    • Added extensive automated test coverage for the new iOS validations and the underlying parsing utilities, including early-return and gating behaviors.
  • Documentation

    • Added an implementation-ready specification for the iOS prescan expansion and its acceptance criteria.

…nts/appicon) + shared capacitorMajor

Shared, never-throwing parsing infrastructure the new iOS prescan checks
depend on (spec parsing-infrastructure section):
- checks/ios-plist-read.ts: plistString/Bool/HasKey/ArrayStrings/DictBlock
  (promotes the private plistStringValue out of ios-plist.ts, which now imports it)
- ios-pbxsettings.ts: readBuildSetting (Release-preferred scalar), resolvePlistValue
  ($()->pbxproj substitution, the Info.plist false-positive guard), readBuildConfigs,
  readTargetConfigs (scalar keys only; absent == inherited == skip)
- ios-entitlements.ts: readAppEntitlements + entString/entArray/entBool, plus
  MobileprovisionDetail.profileEntitlements (capability keys from the profile's
  Entitlements dict) in mobileprovision-parser.ts
- ios-appicon.ts: readContentsJson (JSON.parse, never throws) / appIconSetDir /
  hasMarketingIcon
- capacitor-version.ts: shared capacitorMajor (extracted from android-project.ts,
  generalized to check core/ios/android; android-project re-imports it)

All pure parsers return null/[]/{} on malformed input. Grounded against the real
Capacitor-8 SPM tutorial project (scans clean). flow.ts synthesis sites carry the
new required profileEntitlements field.

Claude-Session: https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A
…tor config, pods/spm, app icons)

Wire 33 new iOS prescan checks into ALL_CHECKS (47 -> 80 total):
- 10 plist (Info.plist / App Store): bundle-id/version formats, encryption
  compliance, ATS arbitrary loads, launch storyboard, orientations, display
  name, background modes.
- 7 xcode (project / build settings): deployment-target floor vs Capacitor
  major, signing team (suppressed when a provisioning map is present),
  bundle-id mismatch across configs, leftover ENABLE_BITCODE, Swift version,
  no/multiple app targets.
- 4 entitlements / capabilities: app entitlements vs profile, aps-environment
  vs distribution mode, associated-domains + app-groups format.
- 3 capacitor config: shipped server.url, server.cleartext, allowNavigation
  wildcard.
- 9 pods/spm/app icons: pods not installed / lock missing / capacitor missing,
  SPM Package.resolved missing / capacitor dependency missing, app icon
  empty / referenced-file-missing / marketing-missing, SPM deployment-target
  consistency.

Builder-grounded severity (spec deviation 2, verified against
capgo_builder_new github-ios-build.yml + fastlaneTemplateIos.ts): the cloud
build runs pod install and resolves SPM during build_app, and actool tolerates
an empty AppIcon for non-App-Store exports, so pods/spm not-installed/resolved
and appicon-empty are warnings (appicon-empty + appicon-marketing upload-gated
back to error). Spec deviation 1 (cut ios/plist-iphoneos-required) honored.

Regression baseline: the real Capacitor-8 SPM tutorial-app scans clean of all
new checks in its real ad_hoc distribution (zero new-check findings); the only
app_store-mode signal is a correct true-positive (committed
aps-environment=development).

Claude-Session: https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A
- aps-environment-vs-mode: downgrade the standalone development+app_store branch from a hard error to a warning (the default Capacitor leftover is benign on a push-free app; the cloud builder neither rewrites entitlements nor fails the archive). Escalate to error only with independent push evidence (Info.plist UIBackgroundModes contains remote-notification); the mapped-profile production mismatch stays an error. Restores the spec clean-scan baseline.
- entitlements-vs-profile-capability: recognize the resolved-team wildcard form <teamid>.* (signed wildcard-App-ID profiles store this, never $(AppIdentifierPrefix)*), and normalize $(AppIdentifierPrefix)/$(TeamIdentifierPrefix) app prefixes vs the resolved team prefix before the subset compare.
- mobileprovision-parser: parse profileEntitlements GENERICALLY (every key in the Entitlements dict by sibling value tag) instead of a fixed ~10-key allowlist, removing the app-vs-profile asymmetry that false-positived granted-but-non-allowlisted capabilities (App Attest, Sign in with Apple, Siri, ...).

Claude-Session: https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A
Grounded against the real Capacitor 8 SPM tutorial-app, documents the new
plist/Xcode/entitlements/Capacitor-config/pods-SPM/app-icon checks, the
parsing helpers, and the false-positive guards.

Claude-Session: https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cecc204e-4c88-42f7-9600-34d8eeec85fd

📥 Commits

Reviewing files that changed from the base of the PR and between 8de18d9 and 727c621.

📒 Files selected for processing (1)
  • .typos.toml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

📝 Walkthrough

Walkthrough

Expands the iOS prescan engine from 47 to 80 checks by adding five new iOS check modules (Xcode build settings, Info.plist validation, entitlements/capabilities, Capacitor config security, Pods/SPM/AppIcon assets), backed by new shared parsing primitives for plist, pbxproj, entitlements, and app icon reading. Extracts profileEntitlements from mobileprovision profiles and refactors capacitorMajor into a shared helper.

Changes

iOS Prescan Expansion

Layer / File(s) Summary
Shared iOS parsing primitives
cli/src/build/prescan/capacitor-version.ts, cli/src/build/prescan/checks/ios-plist-read.ts, cli/src/build/prescan/ios-pbxsettings.ts, cli/src/build/prescan/ios-appicon.ts, cli/src/build/prescan/ios-entitlements.ts, cli/test/prescan/capacitor-version.test.ts, cli/test/prescan/ios-parsers.test.ts
Introduces capacitorMajor (extracted from android-project.ts), regex-based plist readers (plistString/plistBool/plistArrayStrings/plistDictBlock), pbxproj scalar build-setting extraction with $(VAR) substitution, app icon Contents.json reader with marketing icon detection, and App.entitlements reader with typed accessors. All helpers are covered by unit and grounding tests.
profileEntitlements extraction from mobileprovision
cli/src/build/mobileprovision-parser.ts, cli/src/build/onboarding/ios/flow.ts, cli/test/prescan/ios-parsers.test.ts
Adds ProfileEntitlementValue, ProfileEntitlements types and profileEntitlements field to MobileprovisionDetail; implements extractProfileEntitlements() that parses the plist Entitlements dict and skips auto-managed keys. Wires the field into synthesizeProfileFromAscSummary and the import-provide-profile-path flow.
Xcode pbxproj prescan checks
cli/src/build/prescan/checks/ios-xcode.ts, cli/src/build/prescan/checks/android-project.ts, cli/test/prescan/checks-ios-xcode.test.ts
Adds seven PrescanCheck exports: deploymentTargetCapacitor, signingTeam (suppressed by provisioning map), bundleIdMismatchAcrossConfigs, enableBitcodeLeftover, swiftVersionSanity, noAppTarget, multipleAppTargets. Refactors android-project.ts to import capacitorMajor. Tests include pbxproj fixture builder, grounding baseline, and per-check mutation cases.
Info.plist prescan checks and ios-plist.ts refactor
cli/src/build/prescan/checks/ios-plist-checks.ts, cli/src/build/prescan/checks/ios-plist.ts, cli/test/prescan/checks-ios-plist-checks.test.ts
Adds ten PrescanCheck exports covering bundle ID format, version format, ATS arbitrary loads (upload-escalated), encryption compliance, launch storyboard, iPad multitasking orientations, display name, and background modes. Refactors ios-plist.ts to use shared plistString instead of a local parser.
Entitlements vs profile and Capacitor config checks
cli/src/build/prescan/checks/ios-entitlements-checks.ts, cli/src/build/prescan/checks/ios-capacitor-config.ts, cli/test/prescan/checks-ios-entitlements-config.test.ts
Adds four entitlements checks (entitlementsVsProfileCapability, apsEnvironmentVsMode, associatedDomainsFormat, appGroupsFormat) and three Capacitor config checks (serverUrlShipped, serverCleartext, allowNavigationWildcard). Tests cover wildcard normalization, APS environment divergence, and server URL/cleartext/navigation-wildcard conditions.
Pods/SPM/AppIcon prescan checks
cli/src/build/prescan/checks/ios-pods-assets.ts, cli/test/prescan/checks-ios-pods-assets.test.ts
Adds nine PrescanCheck exports for CocoaPods installation/lock/Capacitor-wiring, SPM Package.resolved/Capacitor-dependency, AppIcon asset catalog (emptiness/referenced-file/marketing-icon), and SPM-vs-pbxproj deployment target consistency. Tests cover Pods vs SPM project layout discrimination and upload-gated severity escalation.
Registry wiring and engine count update
cli/src/build/prescan/registry.ts, cli/test/prescan/engine.test.ts
Imports all new iOS check modules and adds them to ALL_CHECKS, expanding the registry from 47 to 80 checks. Updates the engine test assertion to match the new count and IDs.
iOS prescan expansion design spec
docs/superpowers/specs/2026-06-22-prescan-ios-expansion-design.md
Adds the implementation-ready specification covering baseline fixture facts, parsing infrastructure requirements, full check enumeration with severity/appliesTo rules, registry wiring plan, and TDD acceptance fixtures.
Typos configuration update
.typos.toml
Adds ITMS and loca to recognized word list.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Cap-go/capgo#2211: Introduced the MobileprovisionDetail interface and parseMobileprovisionBufferDetailed function that this PR directly extends with the profileEntitlements field.

Suggested reviewers

  • riderx
  • cursor
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: expansion of iOS pre-scan checks across multiple categories (plist, Xcode, entitlements, Capacitor config, pods/SPM, app icons).
Description check ✅ Passed The description is comprehensive and well-structured with sections on new checks by group, parsing helpers, severity calibration, upload-gated checks, real project verification, and test results. It covers the required template sections and provides adequate detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq

codspeed-hq Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 97.45%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 42 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
/updates manifest response with metadata 228.2 µs 115.6 µs +97.45%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing wolny/prescan-ios-expansion (727c621) with main (1c01553)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 Builder onboarding TUI preview — ❌ failed

▶ Open the interactive HTML report (zoomable journey tree + cast playback)

Commit: 727c621 · Job summary with the result table

- plistDictBlock: balanced <dict> depth scan instead of lazy first-</dict>
  capture, so a nested NSExceptionDomains dict preceding NSAllowsArbitraryLoads
  no longer truncates the ATS block (fixes ATS false negative).
- appEntitlementKeys: collect only TOP-LEVEL entitlement keys; skip past a
  dict/array value's matching close so nested keys do not leak into the
  capability set feeding the ERROR-severity profile-coverage check.
- Replace external machine-specific tutorial-app absolute paths in the prescan
  grounding tests with self-contained inline real-shaped fixtures so the
  grounding assertions are real on CI (plist/xcode/parsers/entitlements/cap-ver).

Claude-Session: https://claude.ai/code/session_01KwFbH9dxYYFCGR554WRp4A
@WcaleNieWolny WcaleNieWolny marked this pull request as ready for review June 22, 2026 19:28
cursor[bot]
cursor Bot approved these changes Jun 22, 2026
@coderabbitai coderabbitai Bot added the codex label Jun 22, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving: this large iOS prescan expansion (~5k lines, 33 new checks) exceeds the low-risk approval threshold. Cursor Bugbot did not run on this PR (signal skipped). Requesting human review from riderx and Dalanir.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot requested review from Dalanir and riderx June 22, 2026 19:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/build/prescan/capacitor-version.ts`:
- Around line 26-30: The code assumes `range` is always a string before calling
`.match()` on line 29, but package.json can contain non-string dependency values
(numbers, objects, etc.) which will cause a runtime error. Add a type guard to
check if `range` is a string before calling the `.match()` method. If `range` is
not a string, return null to maintain the contract that this function never
throws. This guard should be added after the initial null check on line 27 but
before the `.match()` call on line 29.

In `@cli/src/build/prescan/checks/ios-entitlements-checks.ts`:
- Around line 148-180: The for loop iterating through parseProvisioningMap(ctx)
is checking the same app entitlements against all provisioning profiles in the
map, including those for extensions and watch apps. This causes false errors
when the app entitlements don't match unrelated bundle ID profiles. Scope the
capability comparison to only the primary app profile by filtering
parseProvisioningMap(ctx) to include only the profile that corresponds to the
main app being checked, not all bundle IDs in the map.

In `@cli/src/build/prescan/checks/ios-pods-assets.ts`:
- Around line 355-381: The check is using readBuildSetting to retrieve the
IPHONEOS_DEPLOYMENT_TARGET value in both the appliesTo method and the run
method, but this approach can return a higher project/other-target Release value
first, missing the lower app-target value that actually determines this check's
behavior. Replace the readBuildSetting calls for IPHONEOS_DEPLOYMENT_TARGET with
a method that properly aggregates or retrieves the app-target deployment target
instead of just the first value found. This will ensure the check accurately
identifies the actual app-target deployment target that drives the validation
logic.

In `@cli/src/build/prescan/checks/ios-xcode.ts`:
- Around line 52-58: The presentDeploymentTarget function currently relies on
readBuildSetting which returns only the first Release configuration value
encountered, rather than checking all relevant configurations across project and
app targets. Modify the function to retrieve deployment target values from all
applicable configurations (both project and app targets) and return the lowest
value found among them, instead of just taking the first result from
readBuildSetting. This ensures you catch the actual minimum deployment target
requirement that could block builds.

In `@cli/src/build/prescan/ios-pbxsettings.ts`:
- Around line 145-149: The regex pattern in the match call within the
resolvePlistValue function currently allows mismatched delimiters like $(VAR} by
using independent character classes for opening and closing brackets. Update the
regex to enforce matching delimiters by using alternation to match either
$(VAR_NAME) or ${VAR_NAME} patterns exclusively, ensuring the opener and closer
are paired correctly rather than allowing any combination of ( with ) or { with
}.

In `@cli/test/prescan/checks-ios-pods-assets.test.ts`:
- Around line 382-396: Add a new test case to the
ios/spm-deployment-target-consistency describe block that creates a regression
test for mixed deployment target configurations. The test should use it() to
create a fixture via cleanSpmFiles() where the project-level
IPHONEOS_DEPLOYMENT_TARGET is higher than the app target
IPHONEOS_DEPLOYMENT_TARGET, and the app target is lower than the minimum
specified in Package.swift. Then call spmDeploymentTargetConsistency.run(ctx)
and verify that it correctly warns about the inconsistency, ensuring the check
does not depend on configuration traversal order.

In `@cli/test/prescan/checks-ios-xcode.test.ts`:
- Around line 269-275: The test titled "reads the app-target value too (target
below floor errors even if project-level is fine)" claims project-level settings
are fine but doesn't actually set a project-level IPHONEOS_DEPLOYMENT_TARGET in
the projectSettings object. Update the makePbx call to include
IPHONEOS_DEPLOYMENT_TARGET in the projectSettings parameter (set to a value
above the floor like '13.0') while keeping the targetDebug and targetRelease
IPHONEOS_DEPLOYMENT_TARGET values at '11.0'. This ensures the test properly
validates the "lowest applicable value wins" behavior where the app target's
lower deployment target takes precedence over the project-level setting.

In `@docs/superpowers/specs/2026-06-22-prescan-ios-expansion-design.md`:
- Around line 324-330: Update the iOS checks module listing in the spec to match
the actual implementation: replace the module name `ios-plist-store.ts` with
`ios-plist-checks.ts`, replace `ios-deps.ts` with `ios-pods-assets.ts`, and
adjust the total check count from 34 to 33 checks to align with the implemented
registry. Additionally, review and update any other sections referenced at lines
342-349 that list module names or check totals to ensure consistency with the
actual check implementation throughout the document.
- Around line 211-212: Escape all unescaped pipe characters in the Markdown
table cells by replacing `|` with `\|` when they appear as literal text (such as
in field names, regex patterns, or code examples) rather than as column
delimiters. This applies to the rows with check IDs
ios/plist-ats-arbitrary-loads and ios/plist-launch-storyboard as well as the
rows at lines 250, 257, and 270. Alternatively, if the content is too complex to
fit cleanly in table cells, move detailed regex/technical specifications below
the table as separate reference sections. Ensure the Markdown table parses
correctly without MD056 errors after making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a73e420-16a5-4102-9ded-32e2e503b195

📥 Commits

Reviewing files that changed from the base of the PR and between 1c01553 and 8de18d9.

📒 Files selected for processing (23)
  • cli/src/build/mobileprovision-parser.ts
  • cli/src/build/onboarding/ios/flow.ts
  • cli/src/build/prescan/capacitor-version.ts
  • cli/src/build/prescan/checks/android-project.ts
  • cli/src/build/prescan/checks/ios-capacitor-config.ts
  • cli/src/build/prescan/checks/ios-entitlements-checks.ts
  • cli/src/build/prescan/checks/ios-plist-checks.ts
  • cli/src/build/prescan/checks/ios-plist-read.ts
  • cli/src/build/prescan/checks/ios-plist.ts
  • cli/src/build/prescan/checks/ios-pods-assets.ts
  • cli/src/build/prescan/checks/ios-xcode.ts
  • cli/src/build/prescan/ios-appicon.ts
  • cli/src/build/prescan/ios-entitlements.ts
  • cli/src/build/prescan/ios-pbxsettings.ts
  • cli/src/build/prescan/registry.ts
  • cli/test/prescan/capacitor-version.test.ts
  • cli/test/prescan/checks-ios-entitlements-config.test.ts
  • cli/test/prescan/checks-ios-plist-checks.test.ts
  • cli/test/prescan/checks-ios-pods-assets.test.ts
  • cli/test/prescan/checks-ios-xcode.test.ts
  • cli/test/prescan/engine.test.ts
  • cli/test/prescan/ios-parsers.test.ts
  • docs/superpowers/specs/2026-06-22-prescan-ios-expansion-design.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment on lines +26 to +30
const range = deps['@capacitor/core'] ?? deps['@capacitor/ios'] ?? deps['@capacitor/android']
if (!range)
return null
const m = range.match(/(\d+)/)
return m ? Number(m[1]) : null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard non-string dependency ranges before calling .match().

On Line 29, range can be a non-string when package.json is syntactically valid but malformed (for example, numeric/object dependency values), which throws at runtime and breaks the documented "never throws" contract.

Proposed fix
-  const range = deps['`@capacitor/core`'] ?? deps['`@capacitor/ios`'] ?? deps['`@capacitor/android`']
-  if (!range)
+  const range = deps['`@capacitor/core`'] ?? deps['`@capacitor/ios`'] ?? deps['`@capacitor/android`']
+  if (typeof range !== 'string' || range.length === 0)
     return null
   const m = range.match(/(\d+)/)
   return m ? Number(m[1]) : null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const range = deps['@capacitor/core'] ?? deps['@capacitor/ios'] ?? deps['@capacitor/android']
if (!range)
return null
const m = range.match(/(\d+)/)
return m ? Number(m[1]) : null
const range = deps['`@capacitor/core`'] ?? deps['`@capacitor/ios`'] ?? deps['`@capacitor/android`']
if (typeof range !== 'string' || range.length === 0)
return null
const m = range.match(/(\d+)/)
return m ? Number(m[1]) : null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/prescan/capacitor-version.ts` around lines 26 - 30, The code
assumes `range` is always a string before calling `.match()` on line 29, but
package.json can contain non-string dependency values (numbers, objects, etc.)
which will cause a runtime error. Add a type guard to check if `range` is a
string before calling the `.match()` method. If `range` is not a string, return
null to maintain the contract that this function never throws. This guard should
be added after the initial null check on line 27 but before the `.match()` call
on line 29.

Comment on lines +148 to +180
for (const { bundleId, base64 } of parseProvisioningMap(ctx)) {
const profileEnt = profileEntitlementsOf(base64)
for (const { key, isArray } of appKeys) {
if (isArray) {
const appMembers = entArray(app.raw, key)
const profileValue = profileEnt[key]
const profileMembers = Array.isArray(profileValue) ? profileValue : []
if (profileMembers.some(isWildcardMember))
continue
// Compare on the prefix-normalized suffix: app members carry the
// $(AppIdentifierPrefix) variable, profile members the resolved team prefix.
const profileSuffixes = new Set(profileMembers.map(entitlementMemberSuffix))
const uncovered = appMembers.filter(member => !profileSuffixes.has(entitlementMemberSuffix(member)))
if (uncovered.length > 0) {
findings.push({
id: 'ios/entitlements-vs-profile-capability',
severity: 'error',
title: `Entitlement "${key}" is not fully covered by the provisioning profile for "${bundleId}"`,
detail: `uncovered ${key} value(s): ${uncovered.join(', ')}`,
fix: 'Enable the capability for this App ID in the Apple Developer portal, regenerate the profile, and re-save credentials (or remove the unused entitlement)',
})
}
}
else if (!(key in profileEnt)) {
findings.push({
id: 'ios/entitlements-vs-profile-capability',
severity: 'error',
title: `Entitlement "${key}" is declared by the app but not granted by the provisioning profile for "${bundleId}"`,
detail: `missing capability: ${key}`,
fix: 'Enable the capability for this App ID in the Apple Developer portal, regenerate the profile, and re-save credentials (or remove the unused entitlement)',
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scope capability matching to the primary app profile, not every mapped profile.

Line 148 loops over all provisioning-map entries, but the check compares a single app entitlements file. If CAPGO_IOS_PROVISIONING_MAP contains extension/watch bundle IDs, this can emit false blocking errors for unrelated profiles.

Proposed fix direction
-    for (const { bundleId, base64 } of parseProvisioningMap(ctx)) {
+    const profiles = parseProvisioningMap(ctx)
+    const appBundleId = resolvePrimaryAppBundleId(ctx, app.raw) // derive from app target context
+    const relevantProfiles = appBundleId
+      ? profiles.filter(p => p.bundleId === appBundleId)
+      : profiles
+    for (const { bundleId, base64 } of relevantProfiles) {
       const profileEnt = profileEntitlementsOf(base64)
       ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/prescan/checks/ios-entitlements-checks.ts` around lines 148 -
180, The for loop iterating through parseProvisioningMap(ctx) is checking the
same app entitlements against all provisioning profiles in the map, including
those for extensions and watch apps. This causes false errors when the app
entitlements don't match unrelated bundle ID profiles. Scope the capability
comparison to only the primary app profile by filtering
parseProvisioningMap(ctx) to include only the profile that corresponds to the
main app being checked, not all bundle IDs in the map.

Comment on lines +355 to +381
appliesTo(ctx): boolean {
if (!hasPackageSwift(ctx))
return false
const pbx = readPbxproj(ctx.projectDir)
if (pbx === null)
return false
return readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET') !== null
},
async run(ctx): Promise<Finding[]> {
const pbx = pbxContent(ctx)
if (pbx === null)
return []
const raw = readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET')
if (raw === null)
return []
const pbxTarget = Number.parseFloat(raw)
if (Number.isNaN(pbxTarget))
return []

const packageSwift = readTextIfExists(packageSwiftPath(ctx.projectDir))
if (packageSwift === null)
return []
const m = packageSwift.match(SPM_MIN_RE)
if (!m)
return []
const spmMin = Number.parseFloat(m[1])
if (Number.isNaN(spmMin) || pbxTarget >= spmMin)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use app-target deployment target aggregation instead of a single readBuildSetting value.

On Line 361 and Line 367, readBuildSetting can return a higher project/other-target Release value first. That can miss the lower app-target value that actually drives this check, causing false clean results.

Suggested patch
-import { readBuildSetting } from '../ios-pbxsettings'
+import { readBuildSetting, readTargetConfigs } from '../ios-pbxsettings'
@@
 const SPM_MIN_RE = /\.iOS\(\.v(\d+)\)/
+const APPLICATION_PRODUCT_TYPE = 'com.apple.product-type.application'
+
+function lowestAppDeploymentTarget(pbx: string): number | null {
+  const values = readTargetConfigs(pbx)
+    .filter(t => t.target.productType === APPLICATION_PRODUCT_TYPE)
+    .flatMap(t => t.configs.map(c => c.settings.IPHONEOS_DEPLOYMENT_TARGET))
+    .filter((v): v is string => v !== undefined)
+    .map(v => Number.parseFloat(v))
+    .filter(v => !Number.isNaN(v))
+  return values.length > 0 ? Math.min(...values) : null
+}
@@
   appliesTo(ctx): boolean {
@@
-    return readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET') !== null
+    return lowestAppDeploymentTarget(pbx) !== null
   },
   async run(ctx): Promise<Finding[]> {
@@
-    const raw = readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET')
-    if (raw === null)
-      return []
-    const pbxTarget = Number.parseFloat(raw)
-    if (Number.isNaN(pbxTarget))
+    const pbxTarget = lowestAppDeploymentTarget(pbx)
+    if (pbxTarget === null)
       return []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
appliesTo(ctx): boolean {
if (!hasPackageSwift(ctx))
return false
const pbx = readPbxproj(ctx.projectDir)
if (pbx === null)
return false
return readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET') !== null
},
async run(ctx): Promise<Finding[]> {
const pbx = pbxContent(ctx)
if (pbx === null)
return []
const raw = readBuildSetting(pbx, 'IPHONEOS_DEPLOYMENT_TARGET')
if (raw === null)
return []
const pbxTarget = Number.parseFloat(raw)
if (Number.isNaN(pbxTarget))
return []
const packageSwift = readTextIfExists(packageSwiftPath(ctx.projectDir))
if (packageSwift === null)
return []
const m = packageSwift.match(SPM_MIN_RE)
if (!m)
return []
const spmMin = Number.parseFloat(m[1])
if (Number.isNaN(spmMin) || pbxTarget >= spmMin)
import { readBuildSetting, readTargetConfigs } from '../ios-pbxsettings'
const SPM_MIN_RE = /\.iOS\(\.v(\d+)\)/
const APPLICATION_PRODUCT_TYPE = 'com.apple.product-type.application'
function lowestAppDeploymentTarget(pbx: string): number | null {
const values = readTargetConfigs(pbx)
.filter(t => t.target.productType === APPLICATION_PRODUCT_TYPE)
.flatMap(t => t.configs.map(c => c.settings.IPHONEOS_DEPLOYMENT_TARGET))
.filter((v): v is string => v !== undefined)
.map(v => Number.parseFloat(v))
.filter(v => !Number.isNaN(v))
return values.length > 0 ? Math.min(...values) : null
}
appliesTo(ctx): boolean {
if (!hasPackageSwift(ctx))
return false
const pbx = readPbxproj(ctx.projectDir)
if (pbx === null)
return false
return lowestAppDeploymentTarget(pbx) !== null
},
async run(ctx): Promise<Finding[]> {
const pbx = pbxContent(ctx)
if (pbx === null)
return []
const pbxTarget = lowestAppDeploymentTarget(pbx)
if (pbxTarget === null)
return []
const packageSwift = readTextIfExists(packageSwiftPath(ctx.projectDir))
if (packageSwift === null)
return []
const m = packageSwift.match(SPM_MIN_RE)
if (!m)
return []
const spmMin = Number.parseFloat(m[1])
if (Number.isNaN(spmMin) || pbxTarget >= spmMin)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/prescan/checks/ios-pods-assets.ts` around lines 355 - 381, The
check is using readBuildSetting to retrieve the IPHONEOS_DEPLOYMENT_TARGET value
in both the appliesTo method and the run method, but this approach can return a
higher project/other-target Release value first, missing the lower app-target
value that actually determines this check's behavior. Replace the
readBuildSetting calls for IPHONEOS_DEPLOYMENT_TARGET with a method that
properly aggregates or retrieves the app-target deployment target instead of
just the first value found. This will ensure the check accurately identifies the
actual app-target deployment target that drives the validation logic.

Comment on lines +52 to +58
function presentDeploymentTarget(pbxContent: string): number | null {
const raw = readBuildSetting(pbxContent, 'IPHONEOS_DEPLOYMENT_TARGET')
if (raw === null)
return null
const n = Number.parseFloat(raw)
return Number.isNaN(n) ? null : n
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Compute deployment target from all relevant configs, not readBuildSetting’s first Release hit.

On Line 53, presentDeploymentTarget calls readBuildSetting, which returns the first Release value it sees rather than the lowest value across project + app targets. This can hide a lower app-target deployment target and skip a real build-blocking finding.

Suggested patch
 function presentDeploymentTarget(pbxContent: string): number | null {
-  const raw = readBuildSetting(pbxContent, 'IPHONEOS_DEPLOYMENT_TARGET')
-  if (raw === null)
-    return null
-  const n = Number.parseFloat(raw)
-  return Number.isNaN(n) ? null : n
+  const values: number[] = []
+
+  for (const cfg of readBuildConfigs(pbxContent)) {
+    if (!cfg.isProjectLevel)
+      continue
+    const raw = cfg.settings.IPHONEOS_DEPLOYMENT_TARGET
+    if (raw === undefined)
+      continue
+    const n = Number.parseFloat(raw)
+    if (!Number.isNaN(n))
+      values.push(n)
+  }
+
+  for (const { target, configs } of readTargetConfigs(pbxContent)) {
+    if (target.productType !== APPLICATION_PRODUCT_TYPE)
+      continue
+    for (const cfg of configs) {
+      const raw = cfg.settings.IPHONEOS_DEPLOYMENT_TARGET
+      if (raw === undefined)
+        continue
+      const n = Number.parseFloat(raw)
+      if (!Number.isNaN(n))
+        values.push(n)
+    }
+  }
+
+  return values.length > 0 ? Math.min(...values) : null
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/prescan/checks/ios-xcode.ts` around lines 52 - 58, The
presentDeploymentTarget function currently relies on readBuildSetting which
returns only the first Release configuration value encountered, rather than
checking all relevant configurations across project and app targets. Modify the
function to retrieve deployment target values from all applicable configurations
(both project and app targets) and return the lowest value found among them,
instead of just taking the first result from readBuildSetting. This ensures you
catch the actual minimum deployment target requirement that could block builds.

Comment on lines +145 to +149
const m = rawValue.match(/^\$[({]([A-Z0-9_]+)[)}]$/i)
if (!m)
return rawValue
return readBuildSetting(pbxContent, m[1]) ?? rawValue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Require matching $(...) / ${...} delimiters in resolvePlistValue.

Line 145 currently accepts malformed refs like $(VAR} because opener/closer are matched independently, which can produce false resolutions.

Proposed fix
 export function resolvePlistValue(rawValue: string, pbxContent: string): string {
-  const m = rawValue.match(/^\$[({]([A-Z0-9_]+)[)}]$/i)
-  if (!m)
+  const paren = rawValue.match(/^\$\(([A-Z0-9_]+)\)$/i)
+  const brace = rawValue.match(/^\$\{([A-Z0-9_]+)\}$/i)
+  const varName = paren?.[1] ?? brace?.[1]
+  if (!varName)
     return rawValue
-  return readBuildSetting(pbxContent, m[1]) ?? rawValue
+  return readBuildSetting(pbxContent, varName) ?? rawValue
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const m = rawValue.match(/^\$[({]([A-Z0-9_]+)[)}]$/i)
if (!m)
return rawValue
return readBuildSetting(pbxContent, m[1]) ?? rawValue
}
const paren = rawValue.match(/^\$\(([A-Z0-9_]+)\)$/i)
const brace = rawValue.match(/^\$\{([A-Z0-9_]+)\}$/i)
const varName = paren?.[1] ?? brace?.[1]
if (!varName)
return rawValue
return readBuildSetting(pbxContent, varName) ?? rawValue
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/prescan/ios-pbxsettings.ts` around lines 145 - 149, The regex
pattern in the match call within the resolvePlistValue function currently allows
mismatched delimiters like $(VAR} by using independent character classes for
opening and closing brackets. Update the regex to enforce matching delimiters by
using alternation to match either $(VAR_NAME) or ${VAR_NAME} patterns
exclusively, ensuring the opener and closer are paired correctly rather than
allowing any combination of ( with ) or { with }.

Comment on lines +382 to +396
describe('ios/spm-deployment-target-consistency', () => {
it('is clean when pbxproj target >= Package.swift min (grounding 15.0 vs .v15)', async () => {
const ctx = makeCtx({ projectDir: makeProject(cleanSpmFiles()) })
expect(await spmDeploymentTargetConsistency.run(ctx)).toEqual([])
})

it('warns when pbxproj target < Package.swift min (the dangerous direction)', async () => {
const ctx = makeCtx({
projectDir: makeProject(cleanSpmFiles({
'ios/App/App.xcodeproj/project.pbxproj': pbxproj('14.0'),
})),
})
const f = await spmDeploymentTargetConsistency.run(ctx)
expect(f.some(x => x.severity === 'warning' && x.id === 'ios/spm-deployment-target-consistency')).toBe(true)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a mixed-config regression case for deployment-target consistency.

Current tests around Line 388 only validate a single IPHONEOS_DEPLOYMENT_TARGET value. Add a fixture where project-level is higher but app target is lower than .iOS(.vN) to ensure this check does not depend on config traversal order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/prescan/checks-ios-pods-assets.test.ts` around lines 382 - 396, Add
a new test case to the ios/spm-deployment-target-consistency describe block that
creates a regression test for mixed deployment target configurations. The test
should use it() to create a fixture via cleanSpmFiles() where the project-level
IPHONEOS_DEPLOYMENT_TARGET is higher than the app target
IPHONEOS_DEPLOYMENT_TARGET, and the app target is lower than the minimum
specified in Package.swift. Then call spmDeploymentTargetConsistency.run(ctx)
and verify that it correctly warns about the inconsistency, ensuring the check
does not depend on configuration traversal order.

Comment on lines +269 to +275
it('reads the app-target value too (target below floor errors even if project-level is fine)', async () => {
const low = { IPHONEOS_DEPLOYMENT_TARGET: '11.0', PRODUCT_BUNDLE_IDENTIFIER: 'app.capgo.plugin.TutorialBuild', DEVELOPMENT_TEAM: 'UVTJ336J2D', CODE_SIGN_STYLE: 'Automatic', SWIFT_VERSION: '5.0' }
const dir = projectWith(makePbx({ targetDebug: { ...low }, targetRelease: { ...low }, projectSettings: { SDKROOT: 'iphoneos' } }))
const findings = await deploymentTargetCapacitor.run(ctx(dir))
expect(findings.length).toBe(1)
expect(findings[0].severity).toBe('error')
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression case where both project and app targets set deployment target.

On Line 269, the test title says “project-level is fine,” but this fixture omits project-level IPHONEOS_DEPLOYMENT_TARGET. Add a case with project-level above floor and app target below floor to lock in the intended “lowest applicable value wins” behavior.

Suggested test addition
 describe('ios/xcode-deployment-target-capacitor', () => {
+  it('errors when app target is below floor even if project-level is above floor', async () => {
+    const low = {
+      IPHONEOS_DEPLOYMENT_TARGET: '11.0',
+      PRODUCT_BUNDLE_IDENTIFIER: 'app.capgo.plugin.TutorialBuild',
+      DEVELOPMENT_TEAM: 'UVTJ336J2D',
+      CODE_SIGN_STYLE: 'Automatic',
+      SWIFT_VERSION: '5.0',
+    }
+    const dir = projectWith(makePbx({
+      projectSettings: { IPHONEOS_DEPLOYMENT_TARGET: '15.0', SDKROOT: 'iphoneos' },
+      targetDebug: { ...low },
+      targetRelease: { ...low },
+    }))
+    const findings = await deploymentTargetCapacitor.run(ctx(dir))
+    expect(findings.length).toBe(1)
+    expect(findings[0].id).toBe('ios/xcode-deployment-target-capacitor')
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('reads the app-target value too (target below floor errors even if project-level is fine)', async () => {
const low = { IPHONEOS_DEPLOYMENT_TARGET: '11.0', PRODUCT_BUNDLE_IDENTIFIER: 'app.capgo.plugin.TutorialBuild', DEVELOPMENT_TEAM: 'UVTJ336J2D', CODE_SIGN_STYLE: 'Automatic', SWIFT_VERSION: '5.0' }
const dir = projectWith(makePbx({ targetDebug: { ...low }, targetRelease: { ...low }, projectSettings: { SDKROOT: 'iphoneos' } }))
const findings = await deploymentTargetCapacitor.run(ctx(dir))
expect(findings.length).toBe(1)
expect(findings[0].severity).toBe('error')
})
it('errors when app target is below floor even if project-level is above floor', async () => {
const low = {
IPHONEOS_DEPLOYMENT_TARGET: '11.0',
PRODUCT_BUNDLE_IDENTIFIER: 'app.capgo.plugin.TutorialBuild',
DEVELOPMENT_TEAM: 'UVTJ336J2D',
CODE_SIGN_STYLE: 'Automatic',
SWIFT_VERSION: '5.0',
}
const dir = projectWith(makePbx({
projectSettings: { IPHONEOS_DEPLOYMENT_TARGET: '15.0', SDKROOT: 'iphoneos' },
targetDebug: { ...low },
targetRelease: { ...low },
}))
const findings = await deploymentTargetCapacitor.run(ctx(dir))
expect(findings.length).toBe(1)
expect(findings[0].id).toBe('ios/xcode-deployment-target-capacitor')
})
it('reads the app-target value too (target below floor errors even if project-level is fine)', async () => {
const low = { IPHONEOS_DEPLOYMENT_TARGET: '11.0', PRODUCT_BUNDLE_IDENTIFIER: 'app.capgo.plugin.TutorialBuild', DEVELOPMENT_TEAM: 'UVTJ336J2D', CODE_SIGN_STYLE: 'Automatic', SWIFT_VERSION: '5.0' }
const dir = projectWith(makePbx({ targetDebug: { ...low }, targetRelease: { ...low }, projectSettings: { SDKROOT: 'iphoneos' } }))
const findings = await deploymentTargetCapacitor.run(ctx(dir))
expect(findings.length).toBe(1)
expect(findings[0].severity).toBe('error')
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/prescan/checks-ios-xcode.test.ts` around lines 269 - 275, The test
titled "reads the app-target value too (target below floor errors even if
project-level is fine)" claims project-level settings are fine but doesn't
actually set a project-level IPHONEOS_DEPLOYMENT_TARGET in the projectSettings
object. Update the makePbx call to include IPHONEOS_DEPLOYMENT_TARGET in the
projectSettings parameter (set to a value above the floor like '13.0') while
keeping the targetDebug and targetRelease IPHONEOS_DEPLOYMENT_TARGET values at
'11.0'. This ensures the test properly validates the "lowest applicable value
wins" behavior where the app target's lower deployment target takes precedence
over the project-level setting.

Comment on lines +211 to +212
| `ios/plist-ats-arbitrary-loads` | warning (→error on upload+dev-config) | local | iOS; Info.plist exists | `d=plistDictBlock(raw,'NSAppTransportSecurity')`; null→[]. If `plistBool(d,'NSAllowsArbitraryLoads')===true`→finding. Escalate to **error** when `willUploadToAppStore(ctx) && (ctx.config?.server?.cleartext===true || ctx.config?.server?.url)`. | Remove NSAllowsArbitraryLoads (or `<false/>`); use scoped NSExceptionDomains; remove server.url/cleartext before release. |
| `ios/plist-launch-storyboard` | error | local | iOS; Info.plist exists | `ok = plistHasKey(raw,'UILaunchStoryboardName') || plistHasKey(raw,'UILaunchScreen')`. !ok→error (ITMS-90475/90096). (Drop the optional storyboard-file-existence sub-check — higher FP, low value.) | Add `UILaunchStoryboardName=LaunchScreen` (Capacitor default) or a UILaunchScreen dict. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape pipe characters in table cells to fix broken Markdown table parsing.

Several rows contain unescaped | inside cell text/regex, which breaks column parsing (MD056). Escape literal pipes (\|) or move complex regex/details below the table to keep the spec renderable.

Also applies to: 250-250, 257-257, 270-270

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 211-211: Table column count
Expected: 6; Actual: 8; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 212-212: Table column count
Expected: 6; Actual: 8; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-06-22-prescan-ios-expansion-design.md` around
lines 211 - 212, Escape all unescaped pipe characters in the Markdown table
cells by replacing `|` with `\|` when they appear as literal text (such as in
field names, regex patterns, or code examples) rather than as column delimiters.
This applies to the rows with check IDs ios/plist-ats-arbitrary-loads and
ios/plist-launch-storyboard as well as the rows at lines 250, 257, and 270.
Alternatively, if the content is too complex to fit cleanly in table cells, move
detailed regex/technical specifications below the table as separate reference
sections. Ensure the Markdown table parses correctly without MD056 errors after
making these changes.

Source: Linters/SAST tools

Comment on lines +324 to +330
- `checks/ios-plist-store.ts` — 11 checks (§2.A)
- `checks/ios-xcode.ts` — 7 checks (§2.B)
- `checks/ios-entitlements-checks.ts` — 4 checks (§2.C)
- `checks/ios-capacitor-config.ts` — 3 checks (§2.D)
- `checks/ios-deps.ts` — 5 checks (§2.E)
- `checks/ios-appicon-checks.ts` — 4 checks (§2.F, incl. spm-deployment-consistency)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sync spec module names and new-check totals with the implemented registry.

This spec section still documents 34 new iOS checks and module names (ios-plist-store.ts, ios-deps.ts, ios-appicon-checks.ts) that do not match the implemented registry/test wiring in this PR cohort (33 new checks, ios-plist-checks.ts, ios-pods-assets.ts). Please align these to avoid future implementation drift.

Also applies to: 342-349

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-06-22-prescan-ios-expansion-design.md` around
lines 324 - 330, Update the iOS checks module listing in the spec to match the
actual implementation: replace the module name `ios-plist-store.ts` with
`ios-plist-checks.ts`, replace `ios-deps.ts` with `ios-pods-assets.ts`, and
adjust the total check count from 34 to 33 checks to align with the implemented
registry. Additionally, review and update any other sections referenced at lines
342-349 that list module names or check totals to ensure consistency with the
actual check implementation throughout the document.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Not approving: this large CLI iOS prescan expansion (~5k lines, 33 new checks) exceeds the low-risk approval threshold. Cursor Bugbot was not present on this PR (signal skipped). Human review already requested from riderx and Dalanir; no additional reviewers assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Approved: Cursor Bugbot was not present on this PR (signal skipped), no applicable approval-policy blockers, and this CLI-only iOS prescan expansion is within the medium-risk threshold with substantial test coverage. Reviewers Dalanir and riderx are already assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant