qa-agent-kit 0.6.0__py3-none-any.whl
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.
- qa_agent_kit-0.6.0.dist-info/METADATA +157 -0
- qa_agent_kit-0.6.0.dist-info/RECORD +32 -0
- qa_agent_kit-0.6.0.dist-info/WHEEL +4 -0
- qa_agent_kit-0.6.0.dist-info/entry_points.txt +6 -0
- qa_agent_kit-0.6.0.dist-info/licenses/LICENSE +21 -0
- qakit/AGENTS.md +48 -0
- qakit/__init__.py +21 -0
- qakit/_cli.py +46 -0
- qakit/docs/README.md +14 -0
- qakit/docs/agents.md +40 -0
- qakit/docs/case-studies.md +146 -0
- qakit/docs/device-qa-protocol.md +426 -0
- qakit/docs/getting-started.md +100 -0
- qakit/docs/ko/device-qa-protocol.md +433 -0
- qakit/docs/manifest.md +96 -0
- qakit/docs/mutation-testing.md +69 -0
- qakit/docs/prose-qa.md +49 -0
- qakit/docs/pypi.md +40 -0
- qakit/docs/report.md +64 -0
- qakit/skills/README.md +51 -0
- qakit/skills/device-qa/SKILL.md +87 -0
- qakit/skills/prose-qa/SKILL.md +49 -0
- qakit/skills/qa-rigor/SKILL.md +105 -0
- qakit/tools/device_lock.py +151 -0
- qakit/tools/device_qa.py +838 -0
- qakit/tools/install_skills.py +120 -0
- qakit/tools/mutate.py +684 -0
- qakit/tools/prose_check.py +291 -0
- qakit/tools/recording_frames.swift +78 -0
- qakit/tools/release_checks.py +287 -0
- qakit/tools/screenshot_contrast.swift +42 -0
- qakit/tools/shots_gif.py +81 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Device QA protocol
|
|
2
|
+
|
|
3
|
+
This protocol is for a solo developer whose QA team is an AI agent. The owner is the one person who builds and ships the app. The agent drives a USB-attached test iPhone with `tools/device_qa.py`. Every check has an ID, a pass criterion, and evidence (xcresult, screenshot, log, App Store Connect read-back). "All tests green" is not "ready to ship". The example app (Pawday) had every test green while preparing 1.0 (2026-10-26) and still missed a required-reason declaration, a review note over 4,000 bytes, and a clipped caption.
|
|
4
|
+
|
|
5
|
+
This document does not repeat the content of the documents below. It points at them by check ID.
|
|
6
|
+
|
|
7
|
+
- The QA principles common to all code (boundaries, assertions, determinism, runtime checkers, the real environment) and the mutation-testing engine: `skills/qa-rigor/SKILL.md`. This document is the Apple-app layer on top of it.
|
|
8
|
+
- Code quality, HIG checklist, and review order: an Apple app quality checklist (your own repo's equivalent document)
|
|
9
|
+
- Scenario manifest and `verify_release_scenarios.py`: a release scenario test protocol (your own repo's equivalent document)
|
|
10
|
+
- Baselines, sanitizer lanes, and `run_crash_free_gates.py`: a crash-free app protocol (your own repo's equivalent document)
|
|
11
|
+
- Accessibility, localization, and performance principles: an accessibility, localization, and performance protocol (your own repo's equivalent document)
|
|
12
|
+
- Upload, metadata, and submission procedure: an App Store release runbook (your own repo's equivalent document)
|
|
13
|
+
|
|
14
|
+
There are four tools. `tools/device_qa.py` (`doctor`, `devmode`, `test --app --suite --variant`, `shots`, `crashes`, `perf`, `upgrade`, `release`, `traits`, `lock`, and the app's `device_qa.json` manifest), the shared UI test helper in the `QAKit` Swift package (`Sources/QAKit/QAKit.swift`: `QA.snap`, `QA.note`, `QA.audit`, `QA.answerSystemAlert`, `QA.sweepButtons`, `QA.assertInFront`), the static release checks in `tools/release_checks.py`, and mutation testing with `tools/mutate.py` (per-app config `release/testing/mutation*.json`; the gate is your release-readiness runner's `mutation` lane).
|
|
15
|
+
|
|
16
|
+
## Run order
|
|
17
|
+
|
|
18
|
+
1. **Readiness.** `python3 tools/device_qa.py doctor --app <slug>` must print `ready`. It checks device pairing, Developer Mode, no passcode, the developer disk image, the signing identity, and the ASC key. If Developer Mode is off, run `device_qa.py devmode reveal` and the owner turns it on in Settings. Record the device environment baseline at this point too (SUITE-09).
|
|
19
|
+
2. **Risk list.** Build the risk list, the quadrant table, and the regression scope from `git diff` since the last release tag. Get the acceptance criteria for changed features from the owner before writing code (PLAN-01 to 03, PLAN-06).
|
|
20
|
+
3. **Smoke.** `device_qa.py test --app <slug> --suite smoke` must be green within 5 minutes before anything else proceeds (FUNC-01). If it is red, stop other work and fix or revert.
|
|
21
|
+
4. **Full suite and variants.** Run `device_qa.py test --app <slug> --suite unit --suite ui --variant default --variant dark --variant large-text --variant small-text --variant english`, which changes device-wide settings between runs. Run counts, failures, and new crashes land in `summary.json` (SUITE-01, STAB-01). The build at this step is Debug, and the evidence says so (BUILD-11).
|
|
22
|
+
5. **QA walkthrough and screenshot review.** `--suite qa` runs `QA.sweepButtons`, `QA.snap`, and `QA.audit` on every screen. Look at the `index.md` contact sheet from `device_qa.py shots <xcresult> --out <dir>` one image at a time (UI-01, A11Y-01, FUNC-06). Anything that catches the eye becomes a bug (DEF-01).
|
|
23
|
+
6. **Fix from a failing test.** For every bug, write the failing test first, fix, then confirm it passes (SUITE-05). Go back to step 3 and repeat until no new defects appear.
|
|
24
|
+
6a. **Mutation gate.** `python3 tools/mutate.py --config <app>/release/testing/mutation.json --changed-since 'latest-tag:<app>/*'` breaks the logic lines changed since the last release one at a time and checks that the tests notice. Every survivor gets a test, then run it again (SUITE-08, the qa-rigor skill).
|
|
25
|
+
7. **Static release checks.** Run `python3 tools/release_checks.py --app <slug> --ipa <ipa>` and the existing gates (`run_crash_free_gates.py`, `verify_release_scenarios.py`). They cover placeholders, links, required-reason APIs, purpose string localization, review note bytes, metadata including What's New, AASA via the Apple CDN, and the entitlements of the upload IPA.
|
|
26
|
+
8. **Release archive on the device.** Archive from a clean tree with the app's `archive_ios.sh` (the example apps Pawday and Elsewhen have one) and produce the upload IPA. Export the same archive once more with method `debugging`, install it on the device, and run the black-box smoke and a cold launch without a debugger (BUILD-01, BUILD-06, BUILD-07, BUILD-11).
|
|
27
|
+
9. **TestFlight soak and ASC confirmation.** Upload and attach with the app's `appstore_release.py --apply --wait-build --metadata --screenshots --attach-build --beta-group --build-version <n>` (the Pawday and Elsewhen form). Run the black-box smoke and the production round trip on the TestFlight install, and read crashes and feedback during the soak (BUILD-08, CK-06, FIELD-01 to 03, the SUB area).
|
|
28
|
+
10. **Ship verdict.** When every exit criterion below is met, send the release_gate result and the "not tested" list to the owner. Submission (`appstore_release.py --apply --submit`) and the release request happen only after the owner gives a verbal go.
|
|
29
|
+
|
|
30
|
+
## Check areas
|
|
31
|
+
|
|
32
|
+
How to read the tables. Severity is the damage if the check is missed. **Blocker** stops the release, **Major** must be fixed or the owner accepts the risk, **Minor** is recorded only. "(apps that use it)" applies only to apps with that feature, and "(human)" needs a human hand. "No tool yet" marks a tool that still has to be built. `devicectl`, `xcresulttool`, `cktool`, `xctrace`, `altool`, and `simctl` are prefixed with `xcrun`, and on the Mac that holds the test iPhone `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` is required (`device_qa.py` sets it).
|
|
33
|
+
|
|
34
|
+
Which binary a check ran on matters as much as the result. Every evidence row names one of the binaries below (BUILD-11).
|
|
35
|
+
|
|
36
|
+
| Binary | What | Used for |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| QA configuration | A Release-optimized build with test hooks compiled in (launch arguments, `URLProtocol` stubs, `-uiState`, `-flag.x`, keychain wipe) | Fault and state injection checks. No tool yet: add a QA configuration to the pbxproj generator. Until then run on Debug and write Debug in the evidence |
|
|
39
|
+
| debugging export | An IPA exported from the pure Release archive with method `debugging` (formerly development); uses CloudKit Development and sandbox APNs | Device install, container copy, confirming hooks are absent. Pass `iCloudContainerEnvironment=Production` when production must be observed |
|
|
40
|
+
| Upload IPA | Method `app-store-connect`, export plist with `manageAppVersionAndBuildNumber=NO` | Entitlements, bundle contents, size audit (BUILD-07, BUILD-12) |
|
|
41
|
+
| TestFlight and App Store install | The binary Apple processed; uses CloudKit Production and production APNs | Black-box suite. Only external manipulation works (`devicectl device uninstall app`, Network Link Conditioner, Settings, a second account) |
|
|
42
|
+
|
|
43
|
+
### 1. Release planning and scope (Planning)
|
|
44
|
+
|
|
45
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| PLAN-01 | Rebuild this release's risk list (ACC table: attributes x components → capabilities) and set test depth by likelihood x impact | The app's `docs/qa/RISK.md` is updated for this version, every high cell (for example 12 points or more) has a technique, a `.tags(.riskHigh)` test, and a charter, and the owner confirmed the impact score of each new capability once | Map `git diff --stat <last-tag>..HEAD` and recurring DEVLOG and KNOWN_ISSUES entries onto components (app, widget, extension, CloudKit, external API clients). No tool yet: risk list generator (fails when a high cell has no tagged test) | Major |
|
|
48
|
+
| PLAN-02 | Fill all four Agile Testing Quadrants for every changed feature | One row per feature with no empty cell: Q1 Swift Testing unit, Q2 XCUITest on the USB iPhone, Q3 exploratory charter, Q4 performance, accessibility audit, security (ATS exceptions, no key exposure under `strings`, Keychain) | Build the table from the git diff and test tags; start with unit 70, integration 20, E2E 10 as the first target. No tool yet: quadrant table generator | Major |
|
|
49
|
+
| PLAN-03 | Pick the regression scope by impact analysis and RCRCRC, but run everything before archiving | There are tests in all six cells, Recent, Core, Risky, Configuration-sensitive, Repaired, Chronic, and the full plan runs when `*.plist`, `*.entitlements`, `PrivacyInfo.xcprivacy`, `project.pbxproj`, or the CloudKit schema changes | Fast loop with `xcodebuild test -only-testing:<Target>/<Suite>`, the full `device_qa.py test --suite unit --suite ui` before archiving | Major |
|
|
50
|
+
| PLAN-04 | Write a "not tested" list and accept residual risk only explicitly | Every charter, field item, or check that did not run has a one-sentence risk, the owner accepted or rejected it in their own words, and the same list went into TestFlight What to Test | The gate collects skipped checks (simulator hang, Settings automation failure, no second device). No tool yet: skipped-check collector | Major |
|
|
51
|
+
| PLAN-05 | A single release_gate prints PASS or BLOCK before upload, before submission, and before release | Every row links machine evidence (xcresult, log, screenshot, ASC read-back), a BLOCK names the failing check and its evidence, and a BLOCK the owner overrides is recorded in DEVLOG and RELEASE_CHECKLIST.md as a risk accepted in their words | Set the criteria before testing and give measurement rows thresholds. No tool yet: release_gate (extend `run_app_release_readiness.py` or your equivalent) | Blocker |
|
|
52
|
+
| PLAN-06 | Write acceptance criteria in the owner's words before code, and have a separate model that has not seen the diff write the acceptance tests | Every changed feature has Given/When/Then criteria and a one-line OK from the owner, and the acceptance tests were written by a different model or session from the criteria alone | When the same agent writes both code and tests, the tests mirror the implementation (ISTQB CTFL §1.5.3, §4.5). Misread requirements are the most frequent miss | Major |
|
|
53
|
+
|
|
54
|
+
### 2. Build artifacts, install, and upgrade (Build)
|
|
55
|
+
|
|
56
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| BUILD-01 | Run the exact Release build on the USB iPhone without a debugger | The installed export's CFBundleVersion and GitCommit are the same before and after the run, Smoke and the full UI plan pass, and 5 cold launches show no new crash, watchdog (0x8badf00d), or jetsam, and the first screen of each launch carries its core data (a live process with an empty card is a failure; see FEED-07) | `device_qa.py release --app <slug> --archive <xcarchive>`: exports that archive with the debugging method, installs it, cold-launches it 5 times with no debugger, checks it is alive each time, screenshots it and pulls the crash and jetsam reports afterwards (`--cloudkit Production` prepares the production round trip). `-exportArchive` method `debugging` → `devicectl device install app` → verify the version with `devicectl device info apps`; drive it with the black-box target (BUILD-08) or a `UseDestinationArtifacts` xctestrun (`test-without-building` overwrites the install with the build-for-testing app) | Blocker |
|
|
59
|
+
| BUILD-02 | Review the Debug versus Release setting differences and keep debug code out of pure Release | Every `-showBuildSettings` difference is on the allowlist, and the debugging export has no `#if DEBUG` symbols, debug menu, or test launch argument handler (the QA configuration is exempt) | Compare `xcodebuild -showBuildSettings` of both configurations against the app's `qa/release_settings_allowlist.txt` and confirm with `strings -a`. No tool yet: settings diff comparator | Major |
|
|
60
|
+
| BUILD-03 | Fresh install first launch, offline first and online later | Onboarding and a meaningful empty state appear, there is no string of context-free permission prompts and no crash, the app recovers when the network returns without a relaunch, and the recording was reviewed | `devicectl device uninstall app` (including App Group sibling apps), then install; on Release cut the network with Network Link Conditioner 100% Loss and record with `devicectl device capture screen-record --duration 30`. Keychain wipe is only possible in the QA configuration | Blocker |
|
|
61
|
+
| BUILD-04 | Install over the previous release (N-1, or N-2 when the storage format changed) and upgrade with real data | No crash on first launch, the migration runs exactly once, onboarding does not reappear, and seeded favorites, votes, settings, cards, and widget configuration survive | Locally install the tag worktree as a debugging export and diff `devicectl device copy from --domain-type appDataContainer` (and `appGroupDataContainer`) before and after. The real path is App Store N-1 → seed data in production → install TestFlight N. No tool yet: upgrade script (archive, seed, container diff) `device_qa.py upgrade --app <slug> --from <previous tag>`: builds the previous source in a temporary worktree and installs it, seeds real-user data with the manifest's `upgrade.seed_file` (a black-box UI test), installs the current build over it and runs the `upgrade.check` test with QA_UPGRADE=1. Still missing: the container diff and the App Store path | Blocker |
|
|
62
|
+
| BUILD-05 | Delete and reinstall, watching keychain and App Group leftovers | Keychain leftovers do not wrongly skip onboarding or reuse dead tokens, App Group data left by a sibling app is used correctly or ignored, and there is no crash on the old format | Seed → `devicectl device uninstall app` (widgets and sibling apps stay) → reinstall → assert state, on both paths: keychain kept and keychain wiped (QA configuration hook) | Major |
|
|
63
|
+
| BUILD-06 | Prove the build attached in ASC is the one that passed the gate, and keep hotfix material | Info.plist `GitCommit` and the build number match in the gate record and the ASC read-back, and after release that SHA has a `vX.Y.Z` tag, the .xcarchive, and the dSYM | Keep `manageAppVersionAndBuildNumber=NO` in the export plist and compare through `GET /v1/appStoreVersions/{id}/build`. No tool yet: gate record comparison (refuse submission and release on mismatch) | Blocker |
|
|
64
|
+
| BUILD-07 | The IPA being uploaded carries production entitlements | The app and every .appex have `aps-environment=production` (push apps), `com.apple.developer.icloud-container-environment=Production`, and the release associated domains and App Groups | Run `release_checks.py --app <slug> --ipa <ipa>` (`codesign -d --entitlements - --xml`) on the method `app-store-connect` IPA. The debugging export correctly carries development values and is not a target | Blocker |
|
|
65
|
+
| BUILD-08 | Smoke the processed TestFlight binary itself | The build number installed via TestFlight matches the candidate and the black-box smoke passes | Launch with `XCUIApplication(bundleIdentifier:)` from a UI test target with no app target dependency, and read the version with `devicectl device info apps --include-default-apps` (whether the flag is needed is unverified). Press Install in the TestFlight app through the same API | Major |
|
|
66
|
+
| BUILD-09 | Bind the identifiers the system remembers across updates in a contract file | Widget kinds, AppIntent type and parameter ids, notification category ids and userInfo keys, BGTask ids, SceneStorage and UserDefaults keys, App Group file names, and URL paths have not changed without a migration | Keep a contract file per app and diff it. BUILD-04 upgrades with an N-1 widget placed and an N-1 local notification scheduled, then taps both. No tool yet: contract file diff gate | Blocker |
|
|
67
|
+
| BUILD-10 | When the Xcode or SDK major changes, review "linked on or after" behavior changes separately | If `DTXcode` differs from the last release, build a release-notes checklist and rerun the full plan and the visual baselines (UI-02) on the newest and the oldest supported runtime | Scan Info.plist for `UIDesignRequiresCompatibility` (ignored on the iOS 27 SDK), the UIScene life cycle (TN3187; the app does not launch without it), and the 25-entry cap on `LSApplicationQueriesSchemes`. No tool yet: SDK change detection and plist key check | Blocker |
|
|
68
|
+
| BUILD-11 | Pin build provenance and record which binary every evidence row used | `git status --porcelain` is empty at archive time with no concurrent edits, and every evidence row names the binary kind and the CloudKit and APNs environment | Use the names in the "Binary" table above. No tool yet: provenance record in release_gate | Major |
|
|
69
|
+
| BUILD-12 | Audit what ships inside the bundle and its size | The .app and .appex contents match the allowlist, there are no golden or parity fixtures, provenance JSON, docs, scripts, or test resources, and size growth is explained | Unpack the upload IPA, compare the listing, and compare `App Thinning Size Report.txt` with the last release. No tool yet: bundle content audit | Major |
|
|
70
|
+
| BUILD-13 | Obey the update rules the upload and the store enforce | Every .appex has the same `CFBundleShortVersionString` and `CFBundleVersion` as the app (ITMS-90473), the build number is higher, `UIRequiredDeviceCapabilities` was kept or relaxed only, and if the deployment target was raised, N-1 decoding (FEED-01) and N-1 queries (CK-02) still work | Assert that the generator (`generate_ios_pbxproj.py`) keeps `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` at project level. No tool yet: upload rule check (add to `release_checks.py`) | Major |
|
|
71
|
+
|
|
72
|
+
### 3. Functional design and oracles (Functional)
|
|
73
|
+
|
|
74
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
75
|
+
|---|---|---|---|---|
|
|
76
|
+
| FUNC-01 | Run a Smoke under 5 minutes on every build that is kept and before every archive; a red Smoke stops other tests on that build | On the device: cold launch, every tab, open a detail, vote and cancel, one deep link, and one background round trip pass, and new core flows join Smoke when they ship | `device_qa.py test --app <slug> --suite smoke` (add a smoke suite to the manifest) | Blocker |
|
|
77
|
+
| FUNC-02 | Apply equivalence partitioning and 3-value boundary analysis to changed inputs, filters, counters, and ranges | Every changed function has a partition table in the test comment and 100% partition coverage (for example radius {<0, 0, (0,1 km], (1,5 km], >max}, reason length {0, 1, max-1, max, max+1}) | Move the table into Swift Testing `@Test(arguments:)` parameterized tests | Major |
|
|
78
|
+
| FUNC-03 | Model multi-condition rules (recommendation, sentence assembly) as decision tables and test every possible column (apps that use it) | Every combination of location permission, WeatherKit result, open now, dog size, my past 👎, network, and Apple Intelligence availability has a passing test, and columns without an expected value were pinned by asking the owner once | Table-driven Swift Testing with injected fakes; pull the conditions from code branches and RECOMMENDATION.md | Major |
|
|
79
|
+
| FUNC-04 | Model screens and life cycle as a state table and cover 100% of valid transitions plus invalid attempts | The state table is in docs/qa, there are tests for double-tap vote, back during save, deep link to a deleted place, and every NavigationLink value of a different type, and screens are verified by accessibility identifier | Events are cold launch, deep link, widget tap, notification tap, `press(.home)`, `activate()`, memory warning, network loss, and permission change (real case: a NavigationLink that put a guide value on a `[Place]` path was silently ignored, 2026-10-25) | Major |
|
|
80
|
+
| FUNC-05 | Claims tour: every claim the product makes has a test and evidence | Every claim in `appstore_config.json` (subtitle, description, What's New, review notes), onboarding copy, empty states, and purpose strings has a test and a screenshot, and either passes, is fixed with the owner's OK, or is removed (for example "updated hourly" means a card is under 60 minutes old at 3 arbitrary times) | Build the claim rows from metadata and string catalogs and attach a `QA.snap` to each. No tool yet: claim table generator | Major |
|
|
81
|
+
| FUNC-06 | Judge golden cases and purpose oracles by what the user sees, not by pipeline numbers | Places the owner named appear correctly on the final screen (top 10 cards, recommendations), there are no places dogs cannot enter (cat cafe), no duplicates within one list, no absurdly distant themes (175 km), and distances and opening hours are plausible | Run the recommendation flow on the device from the golden location and compare screenshots and data dumps with the golden list (Pawday `tools/golden.py`). Loop cold verification until no new defects appear | Blocker |
|
|
82
|
+
| FUNC-07 | Consistency across screens (Product oracle) | The same place's name, rating, distance, open status, and vote count match across list, detail, widget, notification, and share card, and date, time, and distance formats match too | In one test follow a single place through every screen and compare the text from `app.debugDescription` and widget screenshots | Major |
|
|
83
|
+
| FUNC-08 | Compare facts with comparable products and reality | Name, address, hours, and distance of 10 arbitrary places match Apple Maps within tolerance, the WeatherKit temperature matches the Weather app in the same minute, and Elsewhen city times match the Clock app around DST | Capture both screens with `devicectl device process openURL 'https://maps.apple.com/?q=<name>'` and `devicectl device capture screenshot` | Minor |
|
|
84
|
+
|
|
85
|
+
### 4. Test suite integrity (Suite)
|
|
86
|
+
|
|
87
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
88
|
+
|---|---|---|---|---|
|
|
89
|
+
| SUITE-01 | Verify the run itself: executed, skipped, expected-failure counts and the destination | The executed count is above 0 and at least the last release's count and the source's `@Test` and `func test` count, no test is newly skipped, and the destination is the real iPhone UDID | `summary.json` from `device_qa.py test`, `xcresulttool get test-results summary`, `xcresulttool compare --baseline-path`, `xcodebuild test -enumerate-tests` (real case: a fixed list in the generated pbxproj ran 0 of a new test file and reported success, 2026-10-24). No tool yet: count and destination comparison against the last release | Blocker |
|
|
90
|
+
| SUITE-02 | Retries must not turn red into green | A new UI test joins the gate only after 20 reruns on the device pass, a test that failed then passed in a release run is blocked as FLAKY, and a quarantined test carries `withKnownIssue` or `XCTExpectFailure` and a bug link in `qa/flaky.md` | `xcodebuild test -only-testing:<id> -test-iterations 20 -run-tests-until-failure -test-repetition-relaunch-enabled YES`; `-retry-tests-on-failure` only in diagnostic runs; execution order is random | Major |
|
|
91
|
+
| SUITE-03 | Bounded waits and per-test time limits instead of fixed sleeps | UI test files contain no `sleep(` or `Thread.sleep` (bounded polling such as `QA.answerSystemAlert` goes on an exception list), and waits use `waitForExistence`, `wait(for:toEqual:)`, `XCTNSPredicateExpectation`, and timeouts from a constants file | Use `executionTimeAllowance` or the plan's Test Timeouts; the plan's minimum is 60 seconds. No tool yet: sleep check guard | Minor |
|
|
92
|
+
| SUITE-04 | Stable, unique accessibility identifiers on leaf elements, hidden behind page objects | A per-screen identifier dump test fails on duplicates and gaps, identifiers sit on Text, Button, and ScrollView and not on ZStack or GeometryReader, and no test looks up by localized label | Attach `app.debugDescription` per screen with `QA.note` and parse it (real case: a container id shadowed child ids and `.accessibilityElement(children: .combine)` swallowed them, 2026-10-25) | Major |
|
|
93
|
+
| SUITE-05 | Every fixed bug has a confirmation test that fails before the fix and passes after | DEVLOG holds the fail and pass evidence pair, a `.bug(...)` trait or the test name carries the bug id, and no bug is closed without this evidence | Run that single test in `git worktree add /tmp/pre <fix>^`, then again on the fixing commit | Major |
|
|
94
|
+
| SUITE-06 | Test plans exist and are wired into the generated scheme | Every app has Smoke, Full (random order, timeouts, keep all screenshots), L10n, Perf, and Checkers `.xctestplan` files that appear in `xcodebuild -showTestPlans` (0 in the reference repo as of 2026-10-28) | Put the plans into the generator (for example the example app's `generate_ios_pbxproj.py`). Plans cannot pick the build configuration, so Perf's Release comes from `xcodebuild -configuration Release` or a separate scheme whose Test action is Release | Major |
|
|
95
|
+
| SUITE-07 | Run the familiar-problems list (error guessing) as automated attacks before archiving | Every entry in `docs/qa/FAMILIAR_PROBLEMS.md` has an automated check and all pass (pbxproj test list versus disk, container identifiers, blocking CloudKit entry during XCTest, zsh `$args` splitting, http images blocked by ATS), and every late-found bug adds an entry | Each entry calls a small rg or python check from the gate. No tool yet: FAMILIAR_PROBLEMS check bundle | Major |
|
|
96
|
+
| SUITE-08 | Mutation testing measures whether the tests catch real bugs | Every mutant on a logic line changed since the last release tag died (survived and uncovered both 0), or the line carries `mutation-ok: <reason>` for an equivalent mutant. Screen code leaves through the config's `exclude` and the walkthrough and screenshot checks own it | `tools/mutate.py --config <app>/release/testing/mutation*.json --changed-since 'latest-tag:<app>/*'`, and the same lane in your release-readiness runner. SwiftPM runs on the Mac with coverage (10 to 20 s per mutant), Xcode apps on a dedicated simulator (1 to 2 minutes). An app with no tag gets a sampled full sweep, so tag each release. Procedure and pitfalls: the qa-rigor skill | Blocker |
|
|
97
|
+
| SUITE-09 | Match the device environment to the baseline before tests start | Appearance, text size, granted permissions, leftover simulated location, Focus mode, Low Power Mode, battery, free space, pending iOS update, locale, automatic time zone, and iCloud account match the baseline, and are restored afterwards | Run `device_qa.py doctor` first and save `devicectl device info appearance` and the rest (today `device_qa.py` resets to iOS defaults after a variant, not to the original values). No tool yet: environment baseline comparison and restore in doctor | Major |
|
|
98
|
+
|
|
99
|
+
### 5. Interruptions and app life cycle (Interruptions)
|
|
100
|
+
|
|
101
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
102
|
+
|---|---|---|---|---|
|
|
103
|
+
| INT-01 | Interrupt every core flow mid-task and return | For each of (reason entry, loading, voting, sheet, search) x (home then return, Notification Center, Siri, notification banner), the same screen, input values, and scroll position come back, with no duplicate write and no stuck spinner | `XCUIDevice.shared.press(.home)` → `app.activate()`, a drag from the top edge, `XCUIDevice.shared.siriService.activate(voiceRecognitionText:)`. `devicectl device process suspend` and `resume` count separately as timeout and timer stress, not as user interruptions | Major |
|
|
104
|
+
| INT-02 | System termination in the background and termination during a write, then relaunch | Relaunch restores the tab and NavigationPath (SceneStorage), and termination during a save leaves no duplicate or half-written CloudKit record (SceneStorage vanishing after the user swipes the app away is normal) | `press(.home)` → `devicectl device process terminate --kill --pid <pid>` (the default is SIGTERM, which the app can catch) → `devicectl device process launch`; verify writes with `cktool query-records` | Major |
|
|
105
|
+
| INT-03 | Memory warning on heavy screens (photo carousel, map, long list) | 3 warnings during scrolling cause no crash and no input loss, discarded images reappear, and no jetsam report follows | `devicectl device process sendMemoryWarning --pid <pid>` during an XCUITest scroll, paired with STAB-01 | Major |
|
|
106
|
+
| INT-04 | Rotation: launch in landscape and rotate repeatedly with a half-filled form and an open sheet | Landscape launch works, after 5 consecutive rotations values, sheet, keyboard, and safe area are right, and the iPhone `UISupportedInterfaceOrientations` has no portraitUpsideDown (Face ID iPhones ignore it, so upside down is checked on iPad only) | A rotation loop with `XCUIDevice.shared.orientation = .landscapeLeft` or `devicectl device orientation set`; assert Info.plist with `plutil -p` | Minor |
|
|
107
|
+
| INT-05 | A real phone call and screen lock during a write (human) | After the call ends or the lock is released the same screen and typed text remain and there is no duplicate vote | There is no API to make a phone call. While the agent's XCUITest pauses mid-vote and waits, the owner calls the phone and locks and unlocks it | Major |
|
|
108
|
+
| INT-06 | Change text size and dark mode on every screen in front, and again while the app waits in the background | Every screen comes back in the same process it had before the change (no relaunch), and no crash or watchdog (0x8BADF00D scene-update) report is written | `device_qa.py traits --app <slug>`: launches each screen from the manifest's `traits.screens` launch arguments, changes smallest text, the largest accessibility size, xxxL, default, dark, light in front, repeats with Settings in front, returns and compares the pid and the crash reports. iOS asks a waiting app to redraw its snapshot with the new settings and kills it after 10 s. The example app's map redrew about 45 times a second in a loop between the photo row height and the map fit and died (2026-10-28) | Blocker |
|
|
109
|
+
|
|
110
|
+
### 6. Permissions and consent (Permissions)
|
|
111
|
+
|
|
112
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
113
|
+
|---|---|---|---|---|
|
|
114
|
+
| PERM-01 | A permission state matrix for every protected resource the app touches | For location, photos, camera, and the rest: undetermined → allow, deny, allow once, reduced allow (precise location off, limited photos), revoke in Settings while backgrounded, and re-allow all work, the fallback UI is reachable, and recovery needs no relaunch | `app.resetAuthorizationStatus(for: .location)` before launch, dialogs through `QA.answerSystemAlert` (springboard, BusinessActionSheet, ko and en labels), revocation by driving `com.apple.Preferences`. On the iOS 27 simulator the location dialog blocks tree queries, so do this on the real device | Blocker |
|
|
115
|
+
| PERM-02 | Notification permission: deny, provisional, and revoke paths | Core flows work with notifications denied, `getNotificationSettings` is read before scheduling, and revocation shows in the app UI | Xcode 27.1 `XCUIProtectedResource` has no notifications case, so reset by delete and reinstall. Confirm once whether the dialog reappears immediately on iOS 27 | Major |
|
|
116
|
+
| PERM-03 | Permission pre-prompt screens and consent copy | A pre-prompt has one Continue or Next button and no exit such as Later or Not Now, the app is usable without push, location, or tracking, and marketing or briefing pushes have in-app consent and an off toggle. Agree and Do Not Agree on a legally required consent screen are allowed as an HIG exception | Inspect pre-prompt button titles in SwiftUI source (Allow, 허용, 권한, 설정하기, Not Now, 나중에) and check a Settings toggle per notification category. No tool yet: pre-prompt lint | Major |
|
|
117
|
+
| PERM-04 | Audit purpose strings for every protected API that app and SDK code references | Every framework reference in every Mach-O has an `NS*UsageDescription` in base, ko, and en that states the feature and a concrete example in one sentence, the claim matches the SUB-05 data flow list, and no ITMS-90683 mail arrives after upload (5.1.1(ii)) | `release_checks.py` purpose string check (`otool -L`, `nm -u`, `plutil -p`, InfoPlist.xcstrings). In the example app (Pawday) the "precise location stays on device" wording contradicts sending precise coordinates to WeatherKit, and the en InfoPlist is missing | Blocker |
|
|
118
|
+
| PERM-05 | Treat the restricted state (Screen Time) separately from denied (human) | When location is restricted the app explains why and does not show an "Open Settings" button the user cannot act on | The owner sets a Screen Time location restriction on the test iPhone once, and the agent runs the XCUITest assertion | Minor |
|
|
119
|
+
|
|
120
|
+
### 7. Location (Location)
|
|
121
|
+
|
|
122
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
123
|
+
|---|---|---|---|---|
|
|
124
|
+
| LOC-01 | Fixed points: places with no data and outside Korea (apps that use it) | Central Seoul, Busan, Jeju, a rural spot with 0 places, open sea, and Tokyo each show content or an explicit empty state with a sane distance string, and failures of Korea-only APIs outside Korea are handled gracefully | Inside a test use `XCUIDevice.shared.location = XCUILocation(...)`; device-wide use `device_qa.py test --variant seoul` (busan, jeju, and other points go into the manifest). The plan's Simulated Location does not reach the app under UI test | Blocker |
|
|
125
|
+
| LOC-02 | Moving routes with pauses (apps that use it) | On a 2 km walk (1.4 m/s) and a 15 m/s route with pauses the list re-sorts, "near me" refreshes, and there is no spinner loop or request storm (within the NET-06 budget) | `devicectl device simulate location route` takes one speed per run and has no pause option, so chain several runs and hold with `simulate location coordinate` between them. Finish with `simulate location clear` | Major |
|
|
126
|
+
| LOC-03 | Approximate location (precise location off) and accuracy changes (apps that use it) | Under reducedAccuracy (about 5 km quantization, about 4 updates per hour, no region monitoring) ranking and distance labels are honest ("about 3 km"), nothing relies on geofences, and if full accuracy is requested `NSLocationTemporaryUsageDescriptionDictionary` exists | Force the reducedAccuracy path with a QA configuration flag and turn precise location off through Settings automation (unverified on iOS 27.2) | Major |
|
|
127
|
+
|
|
128
|
+
### 8. Network and external services (Network)
|
|
129
|
+
|
|
130
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
131
|
+
|---|---|---|---|---|
|
|
132
|
+
| NET-01 | Inject faults for every external host the app depends on | For every host: timeout, 401, 403, 429, 500, empty body, broken JSON, wrong type, missing field, huge response, stale timestamp, and Low Data Mode show an error or the cache within 10 seconds, retries are bounded, one row does not empty the list, and 401 or 403 disables that source for the rest of the run | `URLProtocol` stubs in the QA configuration (`NET_SCENARIO=offline·slow·500·badjson·host=kakao`) and CKError injection. CloudKit (cloudd) and probably WeatherKit bypass the app's URLSession, so confirm once on the device whether a seam at the call site is needed | Blocker |
|
|
133
|
+
| NET-02 | App launch does not depend on external services | With every host failing or slow, the first screen appears from cache or bundled data within the launch budget (PERF-01), and the launch path has no synchronous network calls or SDK initialization | Cold launch with an all-hosts-fail stub, assert the first screen identifier, and grep App init and scene setup | Blocker |
|
|
134
|
+
| NET-03 | Offline at launch, loss mid-request, and recovery | An interrupted request ends with a message that states the limitation, votes queued offline are sent exactly once after recovery (confirmed with cktool), and network switches lose no data | In the QA configuration switch the stub scenario between steps; on Release and TestFlight cut with Network Link Conditioner 100% Loss and then query server state | Blocker |
|
|
135
|
+
| NET-04 | Real bad networks with Network Link Conditioner (100% Loss, Very Bad Network, 3G) | Smoke passes on every profile with error UI and retry, and whether the USB devicectl and XCTest tunnel survive was confirmed or the row is marked not run | Drive Settings > Developer > Network Link Conditioner through `com.apple.Preferences` (element names unverified on iOS 27.2; devicectl has no network command). If it is flaky, put it on the "not tested" list | Major |
|
|
136
|
+
| NET-05 | IPv6-only (NAT64/DNS64) compatibility | No IPv4 literals in the binary or resources, all traffic goes by host name, and when network code changes ReviewerJourney passes on the Mac's "Create NAT64 Network" Wi-Fi (only an approximation of the review network) | `strings -a` and a resource grep for `([0-9]{1,3}[.]){3}[0-9]{1,3}`; the owner creates the NAT64 Wi-Fi once in System Settings > General > Sharing. No tool yet: IPv4 literal check (add to `release_checks.py`) | Blocker |
|
|
137
|
+
| NET-06 | Per-screen network traffic audit | Every host is on the allowlist, there is no http:// and no key-like query parameter, and per-screen request counts stay within budget so no duplicate requests burn quota | `xctrace record --template Network --device <UDID> --time-limit 60s --launch -- <bundleId>` → parse `xctrace export --har` (confirm once that HAR captures the device URLSession; real case: an http photo URL blocked by ATS). No tool yet: HAR auditor | Major |
|
|
138
|
+
| NET-07 | Quota and API key fitness for the new version (apps that use it) | Calls per session x current DAU stay within WeatherKit's 500,000 per month and the Google and Kakao quotas, and every restricted key succeeds once from the installed Release build | Count calls per launch and per widget hour with a `URLProtocol` counter in UI tests (CloudKit and WeatherKit after the NET-01 seam check). Provider consoles are checked by hand. No tool yet: call counter and estimator | Major |
|
|
139
|
+
| NET-08 | Network faults missing from the stub table: captive portal, DNS failure, TLS failure from a wrong clock, truncated body | A 200 HTML body (hotel or subway Wi-Fi portal) or a wrong content-type is not cached as data, and DNS and TLS failures and truncated bodies on slow networks end in an error state | Add `captive`, `dns`, `tls`, and `truncated` scenarios to the NET-01 stubs (the basis is field experience; there is no primary source) | Major |
|
|
140
|
+
|
|
141
|
+
### 9. CloudKit and shared data (CloudKit)
|
|
142
|
+
|
|
143
|
+
Only for apps that use CloudKit. `cktool query-records` and `delete-records` need a user token that lasts a few hours (or the server-to-server key of the example app's `tools/cloud_export.py`); the management token in the keychain only works for schema commands.
|
|
144
|
+
|
|
145
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
146
|
+
|---|---|---|---|---|
|
|
147
|
+
| CK-01 | Schema match gate before every upload (apps that use it) | Every record type and field the build reads and writes exists in production, predicate fields are QUERYABLE, sort keys SORTABLE, search fields SEARCHABLE, any gap blocks the upload, and the owner's Deploy is confirmed by re-exporting afterwards | Diff `cktool export-schema --environment production` (and development) and run `cktool validate-schema`; pull CKQuery types, NSPredicate fields, and NSSortDescriptor keys from source. Daynest (another app by the owner) has `check_cloudkit_schema.py`. No tool yet: shared schema comparison across apps | Blocker |
|
|
148
|
+
| CK-02 | Old-client compatibility review before deploying a production schema (apps that use it) | There is a table of fields and indexes used by every release version still installed, the pending deploy removes or changes none of them (for example the pending removal of the PlaceVote.voter index), and index removals were tried first in a throwaway container | `git worktree add` per tag to scan CloudKit query code and compare with the development schema to be deployed. Production changes cannot be reverted. No tool yet: old-client field table generator | Blocker |
|
|
149
|
+
| CK-03 | Public database security roles per record type and an adversarial write test (apps that use it) | User-generated types are READ _world, CREATE _icloud, WRITE _creator; settings and flag types are READ _world only; account B cannot edit or delete A's vote and the UI says so gracefully | Parse the GRANT lines of `prod.ckdb` on every export and run an adversarial XCUITest with a second test Apple ID (a human signs in once) | Blocker |
|
|
150
|
+
| CK-04 | Account and CKError state matrix (apps that use it) | Signed out: browsing works and voting explains sign-in; account change re-checks; `.restricted` and `.temporarilyUnavailable` do not wipe the cache or queue work; networkUnavailable backs off, requestRateLimited honors retry-after, serverRecordChanged loses no update, quotaExceeded and notAuthenticated show a clear message | Inject CKError with a QA configuration flag, and check a real sign-out once per release on a device or simulator with no iCloud account | Blocker |
|
|
151
|
+
| CK-05 | Idempotency and concurrency of shared writes (apps that use it) | After a fast double tap, 50 taps, retry after timeout, termination during save, and simultaneous votes from two devices there is exactly one record per voter and place and the reason text is exact | Use deterministic record names for votes and count with `cktool query-records --database-type public --record-type PlaceVote`. The two-device run needs a second device on a different account | Blocker |
|
|
152
|
+
| CK-06 | Production round trip with the Release or TestFlight build (apps that use it) | On a build that talks to production: create 👍, change to 👎 with a reason, delete, and recreate show correctly on the list badge, detail, widget, and share card | Run the black-box bundle (BUILD-08) on the TestFlight install with a test-only Apple ID using external manipulation only, then clean up with CK-08 | Blocker |
|
|
153
|
+
| CK-07 | Public database exposure review (apps that use it) | No field in the public database exposes a user identifier (for example a fixed per-install voter UUID) or personal data beyond what the privacy policy and labels state | Compare production schema fields with the data flow list (SUB-05) (real case: the example app's voter UUID went to every client despite a "no user id" policy, 2026-10-27). No tool yet: schema versus list diff | Blocker |
|
|
154
|
+
| CK-08 | Production test data cleanup (apps that use it) | Writes from TestFlight and production runs use a test Apple ID that can be filtered by creatorUserRecordID, they are deleted after the run so the record count is 0, and real aggregates are untouched | `cktool delete-records ... --environment production --database-type public --dry-run false --yes` (the default is dry-run and asks for confirmation), then confirm 0 with `query-records` | Major |
|
|
155
|
+
| CK-09 | Abuse resistance of crowd signals (votes) (apps that use it) | Charters for re-voting after reinstall, mass creation from a second account, and 100 flips by one voter were run, and whether aggregation dedupes by creatorUserRecordID and the cleanup path are decided | Today the vote key is a per-install UUID in UserDefaults, so a reinstall is a new voter, and _icloud lets anyone CREATE (CloudVotes.swift). No tool yet: abuse charter script | Blocker |
|
|
156
|
+
| CK-10 | CloudKit at real scale (apps that use it) | With 10,000 places and over 1,000 votes on one place in the development container, counts are correct after cursor pagination, `CKError.limitExceeded` (400 records or 2 MB per operation) retries by halving, and the launch budget and memory hold | The `CKQueryOperation` default limit is dynamic and can stop silently at the first page. No tool yet: bulk seed script | Major |
|
|
157
|
+
|
|
158
|
+
### 10. Remote feeds, flags, and backends (Feeds)
|
|
159
|
+
|
|
160
|
+
Only for apps that use remote feeds, flags, or their own backend.
|
|
161
|
+
|
|
162
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
163
|
+
|---|---|---|---|---|
|
|
164
|
+
| FEED-01 | Every installed app version decodes the payload about to be published (apps that use it) | Golden JSON fixtures are in the repo, each tag still in use passes its own decoder tests against the candidate payload, changes are additive only, unknown enums fall back to a default, and arrays decode per element | `git worktree add` per tag and run the core package's `swift test` or the xcodebuild test target against the candidate. A failure blocks publishing, not the app release. No tool yet: per-tag decoder runner | Blocker |
|
|
165
|
+
| FEED-02 | Treat data publishing (for example `today/<sido>.json` from `daily.py`) like a release, with gates and rollback (apps that use it) | Before the pages step, check schema, required fields, per-region row count change (block on a drop over 30%), coordinates inside Korea, URLs, and generatedAt; rollback is `git revert`; the app decodes and only accepts a feed newer than its cache | Add a step that exits 1 and blocks the push on failure, and a UI test that serves a broken feed to confirm the cached card stays. No tool yet: publish gate step | Blocker |
|
|
166
|
+
| FEED-03 | Test the kill switch of risky features in both states (apps that use it) | Shared data, new external integrations, and migrations sit behind a remote flag with a compiled safe default and the last cached value, ON and OFF (pairwise per flag) pass, and a missing, 404, or broken flag file falls back to the default | `flags.json` next to the Pages feed or a CloudKit AppFeatures record (READ _world); force with `-flag.newVoting 0` and `1` in the QA configuration | Major |
|
|
167
|
+
| FEED-04 | Prepare the minimum supported version switch before it is needed (apps that use it) | The remote minimum_version is compared numerically (unit test: 1.9 < 1.10 = 1.10.0 < 2.0), when enforced an undismissable update screen with an App Store link appears, and it is never raised without the owner's OK | Read it from the same remote config as the kill switch, compare `CFBundleShortVersionString` component-wise, and UI test the blocking screen | Major |
|
|
168
|
+
| FEED-05 | Black-box probe of every production dependency hourly and right before submission and release (apps that use it) | Feeds (decoded with the app's real models), AASA, privacy and support URLs, the proxy, and data provider status were green within the last hour, and keys and quotas will last at least 48 hours after submission (90% of reviews finish within 48 hours) | Run curl (status, content-type, size, latency) and a decode CLI built from the core package under launchd, and alert only on failure through the owner's messaging channel. No tool yet: synthetic probe (registered in the server's service table) | Blocker |
|
|
169
|
+
| FEED-06 | User-visible generated sentences use stored facts only (apps that use it) | Every factual sentence in place summaries and region intros traces back to a stored field, and an unsupported "dogs allowed", size policy, health claim, or negative statement about a business blocks publishing (FEED-02) | Compare the output of the example app's `summaries` and `guides` steps (codex exec) with the fields. No tool yet: per-sentence grounding checker | Major |
|
|
170
|
+
| FEED-07 | The production data path crosses its own clock boundaries (apps that use it) | A host test drives the real data path, with no fake-data launch flag, under an injected clock across every boundary the data depends on: just before and after midnight, before and after the morning data job, and the cache expiry. At every instant the main screen has its core data (no blank window), and yesterday's data stays until today's has arrived. Example: Pawday 2026-10-29, weather was blank for up to 22 minutes after midnight because the yesterday card was dropped and not asked again for 15 minutes; every UI and device run used fake weather in the daytime | A clock seam in the data layer (Pawday `WeatherRefresh` host tests), plus one Smoke run per release without fake-data flags. L10N-04 covers formatting at the same instants; this row covers whether the data is there | Blocker |
|
|
171
|
+
|
|
172
|
+
### 11. Entry points: deep links, universal links, widgets, intents (Entry points)
|
|
173
|
+
|
|
174
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
175
|
+
|---|---|---|---|---|
|
|
176
|
+
| ENTRY-01 | Deep link and universal link matrix for every router path (apps that use it) | Path x {cold, warm, background, mid-onboarding} x {valid id, unknown or deleted id, broken, missing, duplicate parameters, percent-encoded Korean, 2,000 characters, uppercase host, trailing slash} lands on the target screen or a graceful fallback, and the collection of old URLs earlier versions left in chat rooms passes too | Cold: `devicectl device process launch --payload-url <url> <bundleId>`; warm: `devicectl device process openURL`; inside a test: `XCUIDevice.shared.system.open(url)`. Build the table from router code and the old URL collection | Blocker |
|
|
177
|
+
| ENTRY-02 | The AASA file is valid as seen from the Apple CDN (apps that use it) | `/.well-known/apple-app-site-association` returns 200 with valid JSON (app IDs, paths) and no redirect, `swcutil verify` passes, and AASA changes ship well before the release (the CDN picks it up within 24 hours, but existing installs re-check about weekly and there is no CDN invalidation) | `release_checks.py` AASA check (through the Apple CDN), `sudo swcutil verify -d <domain> -j aasa.json -u <url>`. Associated Domains developer mode is not evidence | Blocker |
|
|
178
|
+
| ENTRY-03 | Widget taps, App Intents, and quick actions route correctly from every app state (apps that use it) | For every widget kind and family a tap opens the widgetURL destination cold and warm, and intents and shortcuts open the right screen and data | After the owner places the widget once, press it through `XCUIApplication(bundleIdentifier: "com.apple.springboard")` and assert the destination identifier | Major |
|
|
179
|
+
| ENTRY-04 | Outbound app links and real share paths (apps that use it) | "Open in Kakao Map, Naver Map, Instagram" goes to the web, Apple Maps, or the App Store both with and without that app installed and no dead buttons (4.2.3(i)), share links tapped in KakaoTalk, Messages, and Notes work, and a recipient without the app gets a web page (200) with real content | UI test every scheme in `LSApplicationQueriesSchemes` with the target app removed. Chat room share links run as a field item (FIELD-03) | Major |
|
|
180
|
+
| ENTRY-05 | Run App Intents, entity queries, and Spotlight queries headless without placing a widget by hand (apps that use it) | Every intent and query in the app has an AppIntentsTesting test and passes | Use the AppIntentsTesting framework of Xcode 27.1 (API stability and docs unverified) | Minor |
|
|
181
|
+
|
|
182
|
+
### 12. Notifications (Notifications)
|
|
183
|
+
|
|
184
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
185
|
+
|---|---|---|---|---|
|
|
186
|
+
| NOTIF-01 | Local notification paths in foreground, background, and terminated states (apps that use it) | Foreground shows the willPresent banner, in background and terminated states a banner tap opens the exact screen rather than home, and an action taken from the notification shows in the app | Schedule `UNTimeIntervalNotificationTrigger(timeInterval: 5)` through a QA configuration launch argument, then after `app.terminate()` or `press(.home)` press the banner on springboard (banner identifier unverified on iOS 27.2) | Major |
|
|
187
|
+
| NOTIF-02 | APNs sandbox remote push, and production APNs on the TestFlight build (apps that use it) | A sandbox push returns HTTP 200, the payload decodes, the tap routes correctly, and a production push sent to the TestFlight install's token arrives | Get the debugging install's token with `devicectl device copy from --domain-type appDataContainer`; the TestFlight token must be exposed by the app through server registration or a QA screen. `curl --http2 -H 'apns-push-type: alert' https://api.sandbox.push.apple.com/3/device/<token>` needs the owner's APNs .p8 | Major |
|
|
188
|
+
| NOTIF-03 | The 64 local notification cap and travel (apps that use it) | Pending notifications stay under 64 after a long session, and they are rescheduled when the time zone changes | Assert the `getPendingNotificationRequests` count, and re-check schedulers that build times with `Calendar.current` (for example MorningBriefScheduler) after a time zone change | Minor |
|
|
189
|
+
|
|
190
|
+
### 13. Widgets and background work (Widgets & background)
|
|
191
|
+
|
|
192
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
193
|
+
|---|---|---|---|---|
|
|
194
|
+
| BG-01 | Unit test the widget TimelineProvider with code where time moves (apps that use it) | With an injected clock across midnight and DST, entries are at least 5 minutes apart (shorter only for clock widgets after a battery and budget check), the reload policy matches intent, placeholder and snapshot are not empty, and moving times use self-updating formats | Call `getTimeline` with a fake clock. Write moving times with `.relative`, `.offset`, `.timer`, `Text(timerInterval:)`, or iOS 18+ `Text(.currentDate, format:)` (`.time` and `.date` freeze; the Elsewhen 1.2 widget froze for 30 minutes) | Major |
|
|
195
|
+
| BG-02 | Render every widget family per size and appearance (apps that use it) | Family x light and dark x default and AX text x ko and en has no clipping or overlap, and weather data carries attribution (SUB-13) | Use widget previews per family and rendering mode and device home screen screenshots as baselines. ImageRenderer cannot draw Core Animation layers or the WidgetKit environment. Debug with `_XCWidgetFamily` and `_XCWidgetKind` | Major |
|
|
196
|
+
| BG-03 | Widget data matches the app and the app calls reload (apps that use it) | `WidgetCenter.reloadTimelines(ofKind:)` is called after an in-app write, App Group data equals app data at the same moment, and a home screen screenshot within the reload window shows the new data | Compare with `devicectl device copy from --domain-type appGroupDataContainer` on the debugging install, and after the owner places the widget keep before and after `devicectl device capture screenshot` | Major |
|
|
197
|
+
| BG-04 | Force-launch and expire background tasks, and watch background URLSession outside the debugger (apps that use it) | For every BGTask id the run and expiry paths complete (setTaskCompleted, no half cache, os_log confirmed), background URLSession finishes on a home-screen launch, and `_simulate` calls are absent from the release binary | In device LLDB: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"ID"]` and `_simulateExpirationForTaskWithIdentifier:`, then `strings -a` | Major |
|
|
198
|
+
| BG-05 | Locked-state behavior with Data Protection on (apps that use it) (human) | Widgets, notification extensions, and background tasks do not crash or write empty data while locked, and back off gracefully | The test iPhone has no passcode, so Data Protection is off. When code that reads protected files or WhenUnlocked keychain items changes, run once on a device with a passcode | Minor |
|
|
199
|
+
| BG-06 | Widget and icon appearance beyond light and dark (apps that use it) | For every family, default, dark, tinted, and clear home screen, Lock Screen (vibrant), and StandBy device screenshots show photo cards that do not become white blobs, and the app icon is intact in default, dark, clear, and tinted | `devicectl device settings appearance --look-and-feel tinted` (or `clear`) with `--liquid-glass-opacity 0.0` to `1.0`, `devicectl device settings standby --show --duration 30`, then `devicectl device capture screenshot` | Major |
|
|
200
|
+
|
|
201
|
+
### 14. Text input and data attacks (Input & attacks)
|
|
202
|
+
|
|
203
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
204
|
+
|---|---|---|---|---|
|
|
205
|
+
| INPUT-01 | For a Korean-market app: type one jamo at a time on the real Korean 2-set keyboard | Typing "강아지 카페" jamo by jamo yields the right value per syllable, at limit-1, limit, and limit+1 syllables and when stopping mid-syllable at the limit no jamo splits or doubles, search is not destructive on a half-composed syllable, and the limit counts in one unit (Character or utf16) | The owner adds the keyboard once; switch with the globe key and press `app.keys["ㄱ"]`. Assume `typeText` bypasses the IME, and because `markedTextRange` is nil during composition, trimming in onChange breaks input | Major |
|
|
206
|
+
| INPUT-02 | Feed a data type attack table into every input field and server string and verify an exact round trip | Blank, leading space, newline, 255 to 10,000 characters, accents, Asian scripts, ZWJ emoji, flags, Arabic RTL, delimiter characters, and SQL-like values are stored and read back unchanged after `app.terminate()` and relaunch, with no crash, silent truncation, or textClipped audit finding, and the same table injected into server fields through stubs leaves a screenshot per card | Parameterize a shared attack fixture in the UI test target, and compare `String.count` with `utf16.count` wherever there is a limit | Major |
|
|
207
|
+
| INPUT-03 | Paste paths and keyboard feel | Pasting 1,000 Korean characters is stored within the limit, keyboard type and return behavior fit the field, and the focused field is not hidden by the keyboard | `devicectl device pasteboard copy`, then long-press to paste, and `QA.snap` with the keyboard up | Minor |
|
|
208
|
+
| INPUT-04 | Mobile security baseline (OWASP MASVS v2.1.0) | Every release checks STORAGE (os_log `.public`, app switcher snapshot, location and personal data on the pasteboard), NETWORK, PLATFORM (URL scheme input, WebView), CODE (dependency versions), and PRIVACY items, and a sysdiagnose after the session contains no coordinates or ids | Scan the `devicectl device sysdiagnose` output for coordinate and identifier patterns. No tool yet: MASVS table and sysdiagnose check | Major |
|
|
209
|
+
|
|
210
|
+
### 15. Localization, formats, and time (Localization & time)
|
|
211
|
+
|
|
212
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
213
|
+
|---|---|---|---|---|
|
|
214
|
+
| L10N-01 | Sweep every screen per supported language and tricky region | In ko-KR and en-US (and if possible de-DE decimal comma, ar-SA RTL, double-length pseudo-language) there is no English on Korean screens, no raw key, no clipping, no wrong plural or number format, no wrong mirroring, and tests look up by identifier only | `device_qa.py test --variant english` (`QA_LANG`) or `xcodebuild test -testLanguage ko -testRegion KR`, pseudo-language `-NSDoubleLocalizedStrings YES`, RTL `-NSForceRightToLeftWritingDirection YES` (confirm the pseudo-language works on the device) | Major |
|
|
215
|
+
| L10N-02 | Unit test formatters and parsers per locale, clock setting, and calendar | All fixed-format parsing uses `en_US_POSIX` with an explicit TimeZone or `ISO8601DateFormatter`, and display formatters pass under an overridden 12 and 24-hour setting, Buddhist and Japanese calendars, non-Latin digits, and de_DE | Parameterized Locale, Calendar, and TimeZone tests plus a scan for parsing paths without POSIX (when the user overrides 12 or 24-hour, DateFormatter rewrites 'HH:mm') | Blocker |
|
|
216
|
+
| L10N-03 | Device 24-hour override and region change | With the 24-hour setting opposite to the region default every time label and time input is right (release blocker for Elsewhen) | Drive Settings > General > Date & Time through `com.apple.Preferences` (unverified on iOS 27.2). If flaky, add a two-line manual item to "not tested" | Major |
|
|
217
|
+
| L10N-04 | Time-travel tests with an injected clock at DST, midnight, and calendar boundaries | Across time zones {Seoul, New_York, London, Sydney, Santiago, Lord_Howe, Kiritimati (+14), Etc/GMT+12, Kathmandu (+5:45)} x instants {transition ±1 s and 1 min, nonexistent 2026-03-08 02:30 NY, ambiguous 2026-11-01 01:30 NY, 23:59:59, 00:00:00, Feb 29, month end and year end, Santiago 2026-09-06 startOfDay 01:00, the date line}, "today", labels, and widget entries are right | Inject Clock and TimeZone providers and build the boundaries with `TimeZone.nextDaylightSavingTimeTransition(after:)` | Blocker |
|
|
218
|
+
| L10N-05 | "Today" rolls over with the app open, and a time zone change updates the UI immediately | When the date changes the "today" content refreshes, after an overnight run (foreground on charger, Auto-Lock off) the morning shows the new day with flat memory, and a foreground time zone change applies without relaunch | Observe `NSCalendarDayChanged` and `NSSystemTimeZoneDidChange` (or `TimeZone.autoupdatingCurrent`). `significantTimeChangeNotification` is for midnight, carrier time, and DST. Change the time zone through Settings automation (devicectl has no date command) | Major |
|
|
219
|
+
| L10N-06 | Compute a place's time in the place's time zone, not the device's (apps that use it) | Recommendation and opening hours code has no `Calendar.current` or `TimeZone.current` and names an Asia/Seoul calendar, and ReviewerAbroad asserts "open now" and weekends against a KST truth table | Verify with a US time zone UI test (today the example app's `TodayEngine.isOpen` and `isDateInWeekend` use `Calendar.current`). No tool yet: `Calendar.current` check gate | Blocker |
|
|
220
|
+
| L10N-07 | A wrong device clock (automatic setting off, hours or days off) | Freshness labels, cache age, "today", and "open now" do not collapse, and WeatherKit JWT, TLS, and CloudKit failures end in an error state | Move the clock through Settings automation (unverified on iOS 27.2). If flaky, put it on "not tested" (the basis is field experience) | Major |
|
|
221
|
+
|
|
222
|
+
### 16. Accessibility (Accessibility)
|
|
223
|
+
|
|
224
|
+
The principles live in the accessibility, localization, and performance protocol (your own repo's equivalent document); this section defines only what is measured on the device.
|
|
225
|
+
|
|
226
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
227
|
+
|---|---|---|---|---|
|
|
228
|
+
| A11Y-01 | Run the automated accessibility audit on every state of every screen | For every screen x state (empty, loading, loaded, error, offline, permission denied, sheet, alert) `performAccessibilityAudit` reports 0 unexplained issues, and exceptions are filtered only by auditType and element in a single reviewed file | `QA.audit(app, step, in: self)` from page objects; states come from the QA configuration's `-uiState` empty, error, offline. Audit types: contrast, elementDetection, hitRegion, sufficientElementDescription, dynamicType, textClipped, trait | Major |
|
|
229
|
+
| A11Y-02 | Dynamic Type from the smallest to the largest accessibility size, plus Bold Text | At xs, l, xxxl, AX1, and AX5 there is no clipping, overlap, or off-screen control, layouts stack vertically where needed, the Bold Text snapshot passes, and the original setting is restored | `device_qa.py test --variant small-text --variant large-text` (add xxxl and AX1 variants to the manifest; AX sizes need `--larger-accessibility-sizes on`, which `device_qa.py` does not pass yet) with the `.dynamicType` and `.textClipped` audits; Bold Text with `.environment(\.legibilityWeight, .bold)` (real case: a `.lineLimit(1)` caption fixed with ViewThatFits, 2026-10-26) | Major |
|
|
230
|
+
| A11Y-03 | Automated VoiceOver sweep and speech check | The speech log of main ko and en screens has no empty strings, no role-only words ("button"), no image file names, no duplicates, the order makes sense, and a human listens through one main task | `device_qa.py test --variant voiceover` with `XCUIDevice.shared.voiceOverService` `enable`, `moveForward`, `moveIn`, `moveOut`, and `currentSpeech` attached through `QA.note`. There is no activate or double-tap API, so task completion cannot be automated | Major |
|
|
231
|
+
| A11Y-04 | Visual accessibility settings matrix | Under dark and light, Increase Contrast, Reduce Motion, Reduce Transparency, color filters (grayscale, protanopia, deuteranopia), Show Borders, look and feel, Liquid Glass opacity, and Differentiate Without Color: contrast is 4.5:1 up to 17pt and 3:1 at 18pt and above, hit regions are 44x44pt (never below 28), and no state is shown by color alone | `devicectl device settings appearance --mode dark --increase-contrast on --reduce-motion on --reduce-transparency on --color-filter on --color-filter-type protanopia --show-borders on --look-and-feel tinted --liquid-glass-opacity 1.0`, then the `.contrast` and `.hitRegion` audits; Differentiate Without Color through Settings automation (whether Show Borders replaces Button Shapes is unverified). No tool yet: add these flags to the `device_qa.py` variant keys | Major |
|
|
232
|
+
| A11Y-05 | Back Accessibility Nutrition Label claims with passing tests | VoiceOver, Larger Text, and the rest are claimed in ASC only when every common task passes in A11Y-01 to 04 and A11Y-06 | Keep the claim list next to the test results; the owner decides the per-app selection once (voluntary now, mandatory later, date unannounced) | Minor |
|
|
233
|
+
| A11Y-06 | Assistive input beyond VoiceOver: Voice Control, Full Keyboard Access, Switch Control, Assistive Access, Smart Invert | Every tappable element shows in Voice Control "Show names" with a label matching the visible text, swipes and long presses have a voice path, icon buttons (👍/👎) have `accessibilityInputLabels`, keyboard focus order is right, and photos do not invert under Smart Invert | Drive focus with `XCUIElement.typeKey` and mark photos with `accessibilityIgnoresInvertColors`. Switch Control and Assistive Access are checked by a human. No tool yet: inputLabels lint for icon buttons | Major |
|
|
234
|
+
|
|
235
|
+
### 17. Visual review and device and OS coverage (Visual & coverage)
|
|
236
|
+
|
|
237
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
238
|
+
|---|---|---|---|---|
|
|
239
|
+
| UI-01 | Screenshot every step of every flow, keep passing runs too, and review one by one | Every step has a named attachment (for example `03-detail-ko-dark-AX5`), clipping, overlap, low contrast, placeholders, wrong locale, safe area intrusion, fields hidden by the keyboard, and empty states were reviewed one image at a time, and for release builds the contact sheet goes to the owner | `QA.snap` (`.keepAlways`), the plan's Automatic Screen Capture "On, and keep all", and `device_qa.py shots <xcresult> --out <dir>` builds the `index.md` contact sheet | Major |
|
|
240
|
+
| UI-02 | Compare visual regressions against the last release (History oracle) | For every screen x configuration the perceptual difference from the last release baseline is explained by a commit or filed as a bug, and intended changes regenerate the baseline and show before and after pairs to the owner | Pin with `devicectl device simulate statusBar preset screenshot` (9:41) and clear afterwards. Baselines live in the app's `qa/baselines/<config>/`; adding swift-snapshot-testing is the owner's dependency decision. No tool yet: perceptual baseline diff | Major |
|
|
241
|
+
| UI-03 | Run a pairwise configuration matrix instead of one default configuration | Smoke and screenshots run for every PICT row, and risk areas run 3-way: appearance {light, dark}, text {large, AX5}, contrast {on, off}, language {ko, en}, region {KR, US}, location {allow, deny}, network {ok, offline stub} | Generate rows with `pict model.txt` and run `device_qa.py test --variant ...` per row overnight on the USB iPhone. No tool yet: pict install and row-to-variant conversion | Major |
|
|
242
|
+
| UI-04 | Other places the iPhone app gets downloaded: iPad compatibility mode, Apple silicon Mac, Vision Pro | ReviewerJourney passes without clipping on the iPad simulator (iPad cannot be opted out), and Mac and Vision Pro either pass or their opt-out in ASC is recorded | `-destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M5)'`; if the simulator hangs, retry once and otherwise record as not run. `UIRequiredDeviceCapabilities` is not for distribution preference | Major |
|
|
243
|
+
| UI-05 | The smallest and largest iPhone and the iPhone Duo | No clipping or unreachable control on the SE class and Pro Max on the iOS 27.0 runtime and on the iPhone Duo outer and inner screens on the iOS 27.1 runtime | Run the same tour on each simulator, and record a hang as not run rather than a pass (Duo screenshots become mandatory for iOS 27.1 SDK builds from April 2027) | Minor |
|
|
244
|
+
| UI-06 | Actually test the oldest supported iOS | The full suite passed on the runtime equal to the deployment target (iOS 18 for the example apps Pawday and Daynest) or the target was raised to the tested version, availability warnings are errors, and the gate fails when the target is lower than the tested runtime | `simctl runtime list`, `xcodebuild -downloadPlatform iOS -buildVersion 18.x` (Xcode 27.1 support unverified; only 27.0 and 27.1 are installed now). Raising the target is the owner's decision | Major |
|
|
245
|
+
| UI-07 | On-device Foundation Models: the unavailable path on the test iPhone, the available path elsewhere (apps that use it) | When `SystemLanguageModel` is `.unavailable` (deviceNotEligible, appleIntelligenceNotEnabled, modelNotReady) the app falls back to deterministic output with no invented place names, the `PrivateCloudComputeLanguageModel` unavailable reasons (deviceNotEligible, systemNotReady) are covered too, and the available path ran on every release that touched the feature | The test iPhone (an iPhone 14 Pro in the reference setup) is deviceNotEligible, so the fallback path runs automatically. Force each reason with the scheme option "Simulated Foundation Models Availability" (unverified under `xcodebuild test`); the available path runs on a simulator on a macOS Tahoe or later Mac or on the owner's phone, and golden set output is regression-compared with the Evaluations framework | Major |
|
|
246
|
+
| UI-08 | Low-end devices and the 320pt layout | No clipping under Display Zoom on the test iPhone, `os_proc_available_memory` headroom holds and the memory soak passes, and A12 and A13 class devices on iOS 18 (XS, XR, SE 2nd gen) ran on a second-hand second device or are on "not tested" | Turn Display Zoom on once per release and run the screenshot tour | Major |
|
|
247
|
+
| UI-09 | Resizing layouts and postures | State survives on the iPhone Duo outer and inner screens and folding or unfolding mid-task, and nothing breaks under iPadOS 26 window resizing (`UIRequiresFullScreen` is deprecated from 26.0) | Sweep sizes with `devicectl device appResize start` and `set --preferred-size WxH` (supported destinations unverified). Duo postures change only in Device Hub | Major |
|
|
248
|
+
|
|
249
|
+
### 18. Performance and resource conditions (Performance)
|
|
250
|
+
|
|
251
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
252
|
+
|---|---|---|---|---|
|
|
253
|
+
| PERF-01 | Cold launch time against the last release baseline | The `XCTApplicationLaunchMetric(waitUntilResponsive: true)` median is no more than 15% slower than the stored CSV from last time, first frame is within about 400 ms on the test iPhone (iPhone 14 Pro in the reference setup), and the cache appears before the response even on the slow network stub | Release comes from `xcodebuild -configuration Release` or a Release scheme. `measure()` runs iterationCount+1 times and discards the first, so measure a cold launch right after reboot separately, and do not use `-test-iterations` because it multiplies. `xcresulttool export metrics`. No tool yet: comparison against the last CSV | Major |
|
|
254
|
+
| PERF-02 | Scroll and transition hitches on lists, carousels, and the map | The hitch ratio on the main feed, place list, photo carousel, and map pan is under 5 ms/s; 5 to 10 is investigated; 10 and above blocks | `measure(metrics: [XCTHitchMetric(application: app)])` (iOS 26+) or `XCTOSSignpostMetric.scrollingAndDecelerationMetric`, or `xctrace record --template 'Animation Hitches'` | Major |
|
|
255
|
+
| PERF-03 | Memory, CPU, and storage soak and leak tracking | Over 100 detail open-and-close cycles and 50 navigation loops peak memory growth stays within the headroom versus the last release, there is no jetsam, large images are downsampled (a 1.5 MB 4K JPEG decodes to about 31.6 MB), and Leaks shows no app-owned leak before submission | `XCTMemoryMetric`, `XCTCPUMetric`, `XCTStorageMetric`, then `xctrace record --template Leaks --time-limit 60s` and `xctrace export --toc` | Major |
|
|
256
|
+
| PERF-04 | Runtime checkers: ASan, Main Thread Checker, UBSan on the device, TSan on the simulator | The full UI suite on the Checkers-Device configuration reports 0 runtime issues, and simulator TSan reports 0 races or a hang is recorded as not run | `run_crash_free_gates.py --app <App> --run diagnostic --required-only` (your crash-free protocol) or `-enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES`, with longer timeouts (ASan 2 to 5x, TSan 2 to 20x) | Major |
|
|
257
|
+
| PERF-05 | Hangs and main thread blocking | No hang of 250 ms or more in core flows, and no synchronous network, large JSON decode, synchronous SCNetworkReachability call, or synchronous migration on the main thread | `device_qa.py perf --app <slug> --seconds 40`: launches the app, attaches Time Profiler and counts Instruments' potential-hangs and hang-risks (any potential hang fails). `xctrace --launch` waits forever for a boot on an iOS newer than Xcode's SDK, so it attaches instead. In the field: Organizer Hangs and the App Store Connect diagnosticSignatures HANGS | Major |
|
|
258
|
+
| PERF-06 | Low Power Mode and thermal pressure | Smoke and location tests pass in Low Power Mode and discretionary background work stops, and with `thermalState` .serious and .critical injected, heavy work (image prefetch, animation) backs off with no data loss | Turn on Low Power through Settings > Battery automation or a Shortcuts URL via `devicectl device process openURL` (both unverified). Inject thermal state through a QA configuration seam; real thermal conditions and a 4-hour battery drain are checked by hand now and then | Minor |
|
|
259
|
+
|
|
260
|
+
### 19. Stability oracles and robustness (Stability)
|
|
261
|
+
|
|
262
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
263
|
+
|---|---|---|---|---|
|
|
264
|
+
| STAB-01 | Compare crash, watchdog, and jetsam logs before and after every device test session | No new .ips naming the app, widget, or extension, no 0x8badf00d, no JetsamEvent where the app is largestProcess; if one appears the gate fails even when every assertion passed, and the symbolicated log is attached to the bug | `device_qa.py test` collects them automatically at the end, and separately `device_qa.py crashes --app <slug> --since-minutes 60 --out <dir>` (`devicectl device info files` and `copy from --domain-type systemCrashLogs`). Symbolicate with the archive dSYM. No tool yet: extension process name matching | Blocker |
|
|
265
|
+
| STAB-02 | A console log oracle that catches silent failures | The session log has no new "Publishing changes from within view updates", "AttributeGraph: cycle", "Unable to simultaneously satisfy constraints", Thread Performance Checker warning, or swallowed CKError, and new patterns go into FAMILIAR_PROBLEMS | `devicectl device process launch --console` captures standard streams only. Collect os_log with `sudo /usr/bin/log collect --device-udid <UDID> --last 30m` (needs root; `log` is a builtin in zsh). No tool yet: log collection and pattern check | Major |
|
|
266
|
+
| STAB-03 | Run a seeded random explorer (monkey) on the device | Every release runs 3 seeds x 500 steps (30 to 60 minutes of soak for big releases) with `app.state == .runningForeground` at every step and no new crash, and a failing seed is reproduced and filed with its seed | Pick hittable elements from `app.snapshot()` and tap them or type INPUT-02 boundary strings, `QA.snap` every 25 steps, and block external links and purchases with an identifier allowlist. `QA.sweepButtons` is the deterministic sweep. No tool yet: seeded random explorer (no maintained crawler exists for real devices) | Major |
|
|
267
|
+
| STAB-04 | Scripted stress tours | The obsessive tour (👍 50 times, pull to refresh 30 times, sheet 100 times) shows no duplicate records or crashes and flat memory, the rained-out tour (back or dismiss right after an async action) leaves no stale UI or orphan writes, and the back-alley tour (settings, licenses, errors, empty states) renders every screen and passes the audit | One tour test class per app; records through cktool, memory through `XCTMemoryMetric` | Major |
|
|
268
|
+
| STAB-05 | Termination reasons beyond watchdog and jetsam | A new .ips with a Termination Reason fails: 0xdead10cc (file or SQLite lock while suspended, an app and widget App Group bug), 0x2182bad2, 0x2182bad3, 0x2182bad4 (background time exceeded), 0xc00010ff (thermal), 0xbaddd15c (low storage); CPU and disk write resource reports are collected too | Keep a test that suspends mid App Group write. No tool yet: .ips Termination Reason parser (extend `device_qa.py crashes`) | Blocker |
|
|
269
|
+
| STAB-06 | Low storage and a full disk | Writes, caches, SwiftData and Core Data stores, and photo prefetch fail gracefully without data loss, and the app runs even when Caches is emptied between launches | Whether filling with large files through `devicectl device copy to --domain-type temporary` counts against user storage is unverified. No tool yet: disk fill scenario | Major |
|
|
270
|
+
| STAB-07 | Runtime isolation traps in Swift 6 language mode (apps that use it) | On a release that changed the language mode, system callback paths such as notification permission, location delegate, BGTask, CloudKit completion, WidgetKit provider, and Progress ran once each on the device as Release, and the STAB-01 log has no `dispatch_assert_queue` or `swift_task_checkIsolated` trap | Every app is at `SWIFT_VERSION` 5.0 today, so this runs only when the language mode changes. It traps without a compiler warning (SE-0423) | Major |
|
|
271
|
+
|
|
272
|
+
### 20. Exploratory testing (Exploratory)
|
|
273
|
+
|
|
274
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
275
|
+
|---|---|---|---|---|
|
|
276
|
+
| EXP-01 | Run time-boxed exploratory sessions (SBTM) with charters drawn from the diff | Every I SLICED UP FUN letter or tour the release touched has a 45 to 90 minute session, and the sheet in `docs/qa/sessions/<version>/` has charter, area, start time, task breakdown, data, notes, bugs, questions, uncovered areas, and debrief | The agent drives the device with an exploratory XCUITest, leaving `QA.sweepButtons`, `QA.snap`, and `app.debugDescription`, judges with FEW HICCUPPS, and names an oracle per finding. Charters that did not run go on "not tested" | Major |
|
|
277
|
+
| EXP-02 | Three persona soap operas with stacked conditions | Each persona completes one core goal ("find a dog-friendly place open now, vote with a reason, and share") in one XCUITest. A: age 65 (AX5, Increase Contrast, VoiceOver); B: subway commuter (Very Bad Network stub, a 10 m/s route with pauses, Siri mid-vote); C: traveler (Tokyo and Jeju, en_US, Low Data Mode) | Bundle devicectl settings, a location route, and network stubs into one test. One-handed and on-the-move feel belongs to the owner's session (EXP-03) | Major |
|
|
278
|
+
| EXP-03 | A short human test session per release (human) | The owner uses the TestFlight build for 10 to 20 minutes with the charter the agent prepared (new features, risky changes, the screenshot contact sheet, open findings), notes become findings, and without this session the report says "no exploration" | The agent writes the charter and contact sheet and turns the notes into bugs. Automation is checking, not testing | Major |
|
|
279
|
+
| EXP-04 | Usability heuristic review | Every new or changed screen is held against Nielsen's 10 heuristics on its screenshot, and findings carry the heuristic number (actionable error copy, undoing a vote, consistent terms) | Use the UI-01 contact sheet as input | Minor |
|
|
280
|
+
|
|
281
|
+
### 21. App Store submission requirements (Submission)
|
|
282
|
+
|
|
283
|
+
The upload, metadata, and submission procedure is defined by the App Store release runbook (your own repo's equivalent document); this section holds only the checks closed with evidence before submission.
|
|
284
|
+
|
|
285
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
286
|
+
|---|---|---|---|---|
|
|
287
|
+
| SUB-01 | Find placeholder, debug, and hidden-feature leftovers in the pure Release build | User-visible strings and resources contain no Lorem, TODO, FIXME, TBD, placeholder, sample, test, 테스트, 샘플, 디버그, localhost, 127.0.0.1, staging, or example.com, and feature-changing launch arguments, URL paths, and remote keys are removed or written in the review notes | `release_checks.py` placeholder check, `strings -a` plus .xcstrings, JSON, and SwiftUI literal checks, and confirm the debugging export has no `#if DEBUG` symbols (the QA configuration is exempt) | Blocker |
|
|
288
|
+
| SUB-02 | Every link works | URLs in the binary, string catalogs, bundled JSON, ASC metadata (support, marketing, privacy), and review notes return 200 after redirects with a body over 500 bytes and no "Lorem" or "Coming soon", the support page has a real contact, and a UI test reaches the in-app privacy and support links | `release_checks.py` link check; ASC URLs are read from appInfoLocalizations and appStoreVersionLocalizations. GitHub Pages 404s most often after a rename | Blocker |
|
|
289
|
+
| SUB-03 | Required-reason API declarations in every bundle (app, .appex, embedded frameworks) | Every Mach-O that references a required-reason API has its own `PrivacyInfo.xcprivacy` with the category and a reason matching actual use (for example App Group UserDefaults shared with a widget 1C8F.1, app-only CA92.1, own-container timestamps C617.1, systemUptime elapsed 35F9.1) | `release_checks.py` required-reason check; compare `nm -u` and `strings` per archive Mach-O with `plutil -p PrivacyInfo.xcprivacy` (real case: the example app, undeclared, 2026-10-26) | Blocker |
|
|
290
|
+
| SUB-04 | Third-party SDK manifests and signatures | Dependencies in `Package.resolved` and `Podfile.lock` that are on Apple's list have a `PrivacyInfo.xcprivacy` in the archive, binary .xcframeworks have valid signatures, and Apple's list is kept in the repo with a date and refreshed monthly | Intersect resolved packages with the stored list, run `codesign -dv --verbose=4 <x>.xcframework`, and there must be no ITMS-91061 mail. No tool yet: SDK list cross-check | Blocker |
|
|
291
|
+
| SUB-05 | The privacy nutrition label matches the real data flows | A data flow list built from code, the production CloudKit schema, and a packet capture (flow, type, linked, tracking, purpose) matches `NSPrivacyCollectedDataTypes` and the ASC App Privacy answers (3 or more decimals is precise location, free text is Other User Content, a fixed id is linked), and when the list changes the owner re-checks the ASC answers | Capture with `/Library/Apple/usr/bin/rvictl -s <UDID>` and `sudo tcpdump -i rvi0`, but there is a report that rvi0 does not come up on iOS 27 (FB23780118), so an alternative is needed. The Organizer privacy report is GUI-only, so merge per-bundle PrivacyInfo with `plutil`. ASC answers cannot be read with an API key. No tool yet: data flow list generator | Blocker |
|
|
292
|
+
| SUB-06 | Privacy policy content and consistency | The ko and en policies state the data collected, methods, every purpose, equivalent third-party protection, retention, deletion, consent withdrawal, and external AI sharing, with no contradiction against the SUB-05 list (block when "no user id" is stated but one exists) | Fetch the ASC privacyPolicyUrl and the in-app link, compare keywords and data types with the list, and run on every release and whenever the policy is published. No tool yet: policy versus list diff | Blocker |
|
|
293
|
+
| SUB-07 | Account and user-generated content gates | If there is an account (including an automatic guest), in-app account deletion completes end to end, removes shared content, and revokes the Sign in with Apple token; if free text or photos are visible to other users there are filtering, reporting, blocking, a contact, and the age rating UGC answer "yes" (an enum-only 👎 reason is not free text) | Scan public database saves (`publicCloudDatabase.save`, `modifyRecords`) and server POSTs, classify fields as enum, number, or free text, and send new shared fields to the owner for a UGC judgment. No tool yet: shared field classifier | Blocker |
|
|
294
|
+
| SUB-08 | Write the review notes like a spec and cap them in bytes | They cover the concept and audience, what changed and where, devices and OS tested, external services, regional differences and the path a reviewer outside Korea sees (SUB-09), locked features, a non-expiring demo account when needed, and a + formatted phone number, stay within 4,000 UTF-8 bytes (Korean is 3 bytes per character), and were re-read after PATCH | `release_checks.py` review note byte check; build the notes from test logs, the SUB-05 hosts, the SUB-09 steps, and git log, and PATCH appStoreReviewDetails with `appstore_release.py` (real case: build 35 attach 409) | Blocker |
|
|
295
|
+
| SUB-09 | Foreign reviewer scenario: what a fresh device in Cupertino sees (apps that use it) | At US coordinates (37.3349, -122.0090), en_US, fresh install, no iCloud account, no Apple Intelligence, the first screen is not empty, a Korean region picker or sample content appears, and the review notes explain that path | A ReviewerAbroad UI test (`XCUIDevice.shared.location`, `-testLanguage en -testRegion US`) with the L10N-06 KST truth table assertions. Apple does not disclose reviewer locations | Blocker |
|
|
296
|
+
| SUB-10 | What's New covers every user-visible change | Changes since the last approved version from git log, DEVLOG, .xcstrings diff, new screens, new permissions, and new notifications are in whatsNew for every locale (4,000 characters or fewer) and in the review notes, and generic wording is used only for pure bug fix releases | `release_checks.py` metadata lint (What's New). Copy is written in the owner's voice by the repo's writing policy (Codex in the reference setup) | Major |
|
|
297
|
+
| SUB-11 | Screenshots match the submitted build and follow the rules | Re-captured on this build, the perceptual diff against the uploaded set shows no stale UI, more than half of each set shows the app in use, the mandatory "iPhone with Dynamic Island (medium display)" 1179x2556 or 1206x2622 exists (an iPhone 14 Pro captures 1179x2556 natively), 13-inch iPad only when iPadOS is supported, and there is no alpha channel, other platform names, real personal data, or prices | The app's `capture_ios_store_screenshots.sh`, `sips -g hasAlpha`, OCR, compare against downloaded appScreenshotSets, then `appstore_release.py --screenshots` (track the Duo requirement for 2027-04). No tool yet: perceptual diff against the uploaded set | Major |
|
|
298
|
+
| SUB-12 | Text metadata lint | Name 2 to 30 characters, subtitle 30, keywords 100 bytes with each keyword 3 characters or more (the two-character "애견" is not allowed), promotional text 170, description 4,000; no other app or company name (Kakao, Naver, Google, Diningcode, Instagram, competitors) in any metadata field; no price (₩, $, 무료, free) in the name, subtitle, or screenshots; no unverifiable claims (#1, 최고, best) or numbers that differ from real data; and the home screen name is close enough to the store name not to confuse (2.3.8) | `release_checks.py` metadata lint; lengths measured in `.encode('utf-8')` bytes | Major |
|
|
299
|
+
| SUB-13 | Apple Weather attribution for WeatherKit data (apps that use it) | Wherever weather data appears there is the " Weather" mark and the legal link to other data sources (`legalPageURL` returns 200), derived values (a walk index, for example) carry " Weather" and a modification notice, alerts show the Apple alert detail link and the agency's full name with the alert text unaltered (rules for widgets and notifications that cannot hold a link are unverified) | Scan `WeatherService` consumers, put an identifier on the `WeatherService.shared.attribution` view, and assert it in screen tests and widget screenshots. The owner has been rejected under 5.2.5 before | Blocker |
|
|
300
|
+
| SUB-14 | External content rights and attribution ledger (apps that use it) | For every source (Kakao Local, Google Places, Wikimedia Commons, Korea Open Government License, Daum) the terms clause allowing display in an iOS app, the required attribution, and the storage limit are written down, a UI test asserts the attribution element on every screen, Commons CC BY and BY-SA photos show author and license, and the ASC content rights answer is correct | Compare the per-app DATA_TERMS table with code constants, UI identifiers, the server store, published feeds, and map screens (looking at the app cache TTL alone is not enough). No tool yet: rights ledger comparator | Blocker |
|
|
301
|
+
| SUB-15 | Re-derive the age rating answers from the shipped features | Answers derived from code signals (unrestricted web view, UGC, ads, prizes, health topics, messaging, alcohol-related places such as bars and breweries) equal the ASC ageRatingDeclarations, the Age Assurance, Parental Controls, and Social Media items are filled, there is no "For Kids" or "For Children" wording and no image that rasterizes Apple emoji, and changed answers were approved by the owner | Scan the signals, read the ASC values, and compare (socialMedia was newly mandatory once). No tool yet: age signal mapper | Major |
|
|
302
|
+
| SUB-16 | Export compliance declaration backed by evidence | `ITSAppUsesNonExemptEncryption=NO` only when no binary bundles non-OS cryptography; if cryptography is bundled, stop for the owner's legal judgment | `plutil -extract ITSAppUsesNonExemptEncryption raw`, `otool -L` and `nm` per Mach-O for `SSL_`, `EVP_`, and `crypto_box`, and read ASC usesNonExemptEncryption | Major |
|
|
303
|
+
| SUB-17 | Toolchain, upload validation, and asynchronous ITMS warnings | DTSDKName is the iOS 26 SDK or newer (from 2026-04-28), MinimumOSVersion is 13 or higher (from 2026-09-09), `altool --validate-app` passes, processingState is VALID, and no ITMS-90683, 91053, or 91061 mail arrived for this build number | The app's `archive_ios.sh` (includes altool validation), poll `GET /v1/builds?filter[app]=..&filter[version]=..`, and search Gmail for "ITMS-" and the build number | Blocker |
|
|
304
|
+
| SUB-18 | Declared capabilities and background modes match the code | Every UIBackgroundModes entry, BGTaskSchedulerPermittedIdentifiers entry, and entitlement in the app and every .appex has matching code use, and widgets, extensions, and notifications relate to the app and carry no ads | Compare `plutil -p Info.plist` and `codesign -d --entitlements - --xml` per bundle with searches for `allowsBackgroundLocationUpdates`, `BGTaskScheduler.shared.register`, and `registerForRemoteNotifications`. No tool yet: capability versus code comparison (add to `release_checks.py`) | Major |
|
|
305
|
+
| SUB-19 | Native value floor (minimum functionality, 4.2.2) | It is clear the app computes and decides on the device rather than being a web clipping, content aggregator, or link bundle (a place list app with 0 web views still qualifies) | The oracle is not the web view ratio but the description at the top of the review notes and a screen recording (`devicectl device capture screen-record`) that shows what the app computes. The owner settles the wording once per app | Minor |
|
|
306
|
+
| SUB-20 | Use external TestFlight Beta App Review as a rehearsal | A new version line or a build that adds permissions, services, or UGC counts as a rehearsal only when it is the first build sent to an external group on that version, and never as evidence of App Store approval (later builds may skip full review and metadata is not reviewed) | POST betaBuildLocalizations (What to Test), add an external betaGroup, POST betaAppReviewSubmissions, then poll the status (`appstore_release.py --beta-group`) | Major |
|
|
307
|
+
| SUB-21 | Account-level distribution prerequisites | Authenticated ASC API calls run without an agreement 403, territory availability matches intent, the DSA trader status is declared per app (non-trader is valid too), and the public contact choice is recorded | Read appAvailabilities and territoryAvailabilities and make a cheap authenticated call. Agreements and DSA are handled by the owner in the ASC UI only when missing | Major |
|
|
308
|
+
| SUB-22 | For a Korean-market app: Location Information Act location-based service business registration judgment (apps that use it) (human) | The owner's judgment on whether a free one-person app is a "business" is recorded, and if it applies, the report (Article 9, or Article 9-2 for small businesses and one-person startups, within 1 month of starting) and the location-based service terms and consent screen exist | The agent drafts the checklist and the owner decides applicability (in 2021, 90 unregistered operators were referred to the police; Apple §5 requires local law compliance) | Major |
|
|
309
|
+
| SUB-23 | For a Korean-market app: Personal Information Protection Act mandatory policy items and app access permission notice (apps that use it) | The policy lists processed items, retention period, destruction, rights, safety measures, the privacy officer's contact, the basis for overseas transfer and the Article 28-8 (2) items (CloudKit and Google are overseas recipients), and there is the Network Act Article 22-2 notice of required and optional access permissions | Extend the SUB-06 keyword check to these items. Applicability is the owner's legal judgment | Major |
|
|
310
|
+
| SUB-24 | The Apple Maps logo and legal link stay visible (apps that use it) | On map screens the legal link is tappable even with a bottom card floating (HIG Maps: brief covering is allowed, permanent covering is not) | Assert `isHittable` of the legal link in the map screenshot test | Minor |
|
|
311
|
+
| SUB-25 | Regional gates: mainland China ICP and the intended country list | appAvailabilities matches the intended country file, and without a MIIT ICP filing number mainland China is excluded and that decision is recorded | Read ASC appAvailabilities and compare with the file. No tool yet: intended country file and comparator | Minor |
|
|
312
|
+
| SUB-26 | Google Maps Platform terms (apps that use it) | Places content is not shown on a non-Google map, the cache holds only place IDs and 30-day lat/lng, and names, addresses, reviews, ratings, and photos are not stored on the server, in feeds, or in history (Service Specific Terms §14.2 to 14.3, ToS §3.2.3) | Scan the server pipeline (`daily.py` google step, `history.sqlite`, the Pages feed) and every map screen for Google-sourced fields. Today the example app's 30-day googleRating and googlePhotoURL retention and PhotoPin on MapKit are out of line; the allowed route is Places UI Kit (§15.1). No tool yet: Google field scanner | Blocker |
|
|
313
|
+
| SUB-27 | No storage of Kakao Local results (apps that use it) | Kakao Local results (raw and derived) are not stored on the device or server and are used in real-time calls only, or Kakao has allowed it in writing | The nightly kakao step's cross-check storage in the example app is BLOCK until Kakao confirms. The basis is a DevTalk operator reply of 2026-08-20, not the terms text | Blocker |
|
|
314
|
+
| SUB-28 | A rights package for every scraped or derived source (5.2.2) (apps that use it) | The Sunday Diningcode crawl (robots.txt is not terms permission), Instagram and Naver mention counts, and MapKit server cross-checks each have a terms basis or written permission that can be produced on request | Add rows to the SUB-14 ledger with the location of the evidence, such as a contract or email | Major |
|
|
315
|
+
| SUB-29 | US state age verification laws (Texas SB 2420, from 2026-06-04) | The country list excludes the regulated regions or `AgeRangeService.requiredRegulatoryFeatures` is handled, and every release records whether it is a "significant change" (an age rating change counts) | Check the Declared Age Range API, the PermissionKit Significant Change API, StoreKit age rating attributes, and the consent withdrawal server notification | Major |
|
|
316
|
+
| SUB-30 | Review request guard (5.6.1) | Rating requests use only the provided API and a user-tapped write-a-review link, with no custom rating dialog, no "Do you like the app?" gating, and no rewards | Today Elsewhen ClocksView and Daynest TimelineView use the API and Daynest SettingsView uses the link (other apps by the owner). No tool yet: review request check gate | Minor |
|
|
317
|
+
| SUB-31 | Disclose third parties that receive location before sharing (5.1.2(i), 5.1.5) (apps that use it) | For every new recipient a judgment on whether it counts as sharing is recorded once, and if so the purpose string or a consent screen names the recipient | Today the example app sends rounded coordinates to Kakao and the purpose string does not name the recipient (KakaoNearby.swift) | Major |
|
|
318
|
+
| SUB-32 | Icon and brand (2.3.8, 2.3.9, 4.1) | Small, large, and alternate icons resemble each other, the icon and name carry no third-party brand or SF Symbol-like glyph, the rights to an AI-generated icon are recorded, and the 1024 icon has no alpha (Icon Composer commonly triggers ITMS-90717) | `sips -g hasAlpha` and icon set comparison; rights are recorded in DESIGN.md | Major |
|
|
319
|
+
| SUB-33 | Mac App Store branch (2.4.5) (apps that use it) | Sandboxed, no auto-launch at login without consent, no self-updater, no downloaded code, all localizations in one bundle, and the quarantine attribute removed (from 2025-02-18) | For Mac apps such as Elsewhen Mac, check the sandbox with `codesign -d --entitlements - --xml` and quarantine with `xattr` (PlainClips, another app by the owner, was rejected under 2.4.5) | Major |
|
|
320
|
+
| SUB-34 | Generative AI rules (1.2, Foundation Models acceptable use) (apps that use it) | A one-time judgment is recorded on whether an on-device chat tab makes it an "AI content generation app", and guardrail and refusal errors show a safe fallback screen | Keep a UI test that injects `LanguageModelSession.GenerationError` | Major |
|
|
321
|
+
| SUB-35 | State the service region in metadata (apps that use it) | The English subtitle and description do not promise "nearby places" to a US reviewer and say the target is Korea, and the review notes explain regional differences or confirm they are the same (2.3.1(a)) | Add a region wording rule to the `release_checks.py` metadata lint | Major |
|
|
322
|
+
| SUB-36 | No collection of personal data even from public databases (5.1.1(viii)) (apps that use it) | Published feeds and the history store contain no personal data from license records or crawled lists (owner names, personal phone numbers, personal Instagram accounts) | Scan the feeds and `history.sqlite` for name, phone number, and handle patterns. No tool yet: personal data pattern scanner | Major |
|
|
323
|
+
|
|
324
|
+
### 22. TestFlight field testing (Field)
|
|
325
|
+
|
|
326
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
327
|
+
|---|---|---|---|---|
|
|
328
|
+
| FIELD-01 | Use the internal TestFlight soak as the field test stage (human) | Feature releases use the candidate in real life for at least 2 days, and What to Test comes from the risk list and "not tested" (for example turning Wi-Fi off mid-walk) | The agent uploads, writes What to Test, and adds internal testers (up to 100, no review, 90-day expiry) | Major |
|
|
329
|
+
| FIELD-02 | Verify the soak telemetry by machine before submission | The candidate build's betaBuildUsages crashCount is 0, there are no unresolved betaFeedbackCrashSubmissions or ScreenshotSubmissions, and crash logs are symbolicated into blocking issues | During the soak a launchd poller reads `/v1/builds/{id}/metrics/betaBuildUsages` and `/v1/apps/{id}/betaFeedbackCrashSubmissions` hourly with the ASC .p8 JWT from `.local-secrets`. No tool yet: soak poller | Blocker |
|
|
330
|
+
| FIELD-03 | A field-only checklist during the soak (human) | Wi-Fi to cellular handoff on a walk, dead zones, readability in sunlight, real GPS jitter, production push and the morning briefing time, the notification pile-up after a Focus mode ends, universal links tapped in Messages, Notes, and KakaoTalk, and gestures and one-handed use are done or on "not tested" | The agent sends the list with the build and records the owner's answers as findings | Major |
|
|
331
|
+
|
|
332
|
+
### 23. Release, phased rollout, and post-release (Release)
|
|
333
|
+
|
|
334
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
335
|
+
|---|---|---|---|---|
|
|
336
|
+
| REL-01 | Ship non-trivial updates with manual release and phased release | The submission uses "Manually release this version", updates that touch migrations, the CloudKit schema, StoreKit, or core navigation have phased release on (1, 2, 5, 10, 20, 50, 100% over 7 days), and the irreversible release request is sent after the owner's explicit go at a time that allows 48 hours of watching | `POST /v1/appStoreVersionPhasedReleases` at submission, `POST /v1/appStoreVersionReleaseRequests` after approval (cannot be cancelled). A first version has no phased release | Major |
|
|
337
|
+
| REL-02 | Watch the 7-day rollout with pre-agreed pause rules | Check at T+1h, T+24h, and before the day 5 and 6 stages; pause and alert the owner with evidence on a new hang signature in a core flow, data loss or sync reports, reviews mentioning crashes, or a CloudKit error spike; pauses total at most 30 days so there is a day-30 decision point; and removing from sale is not used as a brake | Read perfPowerMetrics, diagnosticSignatures (HANGS, DISK_WRITES, LAUNCHES only; no crashes), and customerReviews, and watch crashes through MetricKit (REL-04) or Organizer. Pause with `PATCH phasedReleaseState=PAUSED`. No tool yet: rollout watch job | Major |
|
|
338
|
+
| REL-03 | Day-zero smoke of the store binary | When the new version appears in the store, update the test iPhone from the App Store, confirm the version, and pass the black-box smoke; a launch failure is escalated immediately | `XCUIApplication(bundleIdentifier: "com.apple.AppStore")` (fragile) or the owner's Updates tab, `devicectl device info apps --include-default-apps`, the BUILD-08 bundle (in 2012 the App Store shipped broken FairPlay binaries) | Major |
|
|
339
|
+
| REL-04 | Self-collected field diagnostics (MetricKit) | The app stores MetricKit diagnostic and metric payloads with version and build and uploads them to an endpoint you control, a forced crash or hang in a debug build arrives on the next launch, and the privacy label covers diagnostics | An `MXMetricManager` subscriber (iOS 18 to 26) or the iOS 27 MetricManager async sequence; pull test payloads from the debugging install with `devicectl device copy from --domain-type appDataContainer`. No tool yet: MetricKit collection endpoint | Major |
|
|
340
|
+
| REL-05 | Post-release watching beyond crashes | For 7 days review new-version reviews daily, check CloudKit Console Telemetry and Logs on days 1 and 3, scan the support mailbox, keep feed probes green, and answer reviews only after the owner's approval | `GET /v1/appStoreVersions/{id}/customerReviews` (reuse the review section of Elsewhen's `asc_metrics.py`), CloudKit Telemetry through a signed-in Playwright browser or the owner, support mail through Gmail | Major |
|
|
341
|
+
| REL-06 | Prepare a fix-forward runbook before release | A hotfix branch can be cut from the release tag, the archive and dSYM exist, a draft expedited review request with reproduction steps exists, there is a decision table between pausing, removing from review, and resubmitting the last good code as a higher version, and the reasons removing from sale is a bad brake are written down (it ends the phased release and goes to everyone on return) | Keep the runbook in the app docs; the owner files expedited review at developer.apple.com/contact/app-store/?topic=expedite (50% of reviews within 24 hours, 90% within 48) | Major |
|
|
342
|
+
| REL-07 | Run a weekly production smoke so the app keeps passing review after release | A scheduled job runs ReviewerJourney and ReviewerAbroad on the installed store or TestFlight build, curls the data endpoints and key status, alerts only on failure through the owner's messaging channel, and is registered in the server's service table | A launchd job on the headless Mac with the USB iPhone (in 2025, 4.0 violations were the second-largest removal reason, 72,271 apps). No tool yet: weekly smoke job | Major |
|
|
343
|
+
| REL-08 | Expiry and deprecation calendar | Check monthly: developer program renewal, domain and Pages, ASC and APNs keys, Kakao and Google key status and pricing, and provider deprecation notices (a lapsed membership halts sales, and on return the release goes to everyone without phasing) | Run a monthly job (Google Places API Legacy was closed to new projects from 2025-03-01 and ends after a 12-month notice). No tool yet: monthly expiry check job | Major |
|
|
344
|
+
|
|
345
|
+
### 24. Defect management and learning (Defects)
|
|
346
|
+
|
|
347
|
+
| ID | Check | Pass criterion | Method and tools | Severity |
|
|
348
|
+
|---|---|---|---|---|
|
|
349
|
+
| DEF-01 | Write complete, minimized bug reports and classify by release-blocking severity | Every bug has an ID, title, build (version and build number), device and iOS, configuration row (language, text size, permissions, network), minimal steps, expected and actual, screenshot or recording, xcresult path, and .ips; is classed S1 crash, data loss, privacy, or rejection risk, S2 core flow broken, S3 degraded with a workaround, S4 cosmetic; and the same bug is looked for on other screens before fixing (RIMGEA) | `docs/qa/bugs/<id>.md` or the KNOWN_ISSUES template; capture with `devicectl device capture screenshot` and `screen-record` | Major |
|
|
350
|
+
| DEF-02 | An escaped-defect ledger feeds the next release | Every bug found after release or by the owner has symptom, cause class (migration, schema, contract, launch path dependency, entitlement, UI state, l10n, time), version, regression test path, and the gate line added, and the gate sets the PLAN-01 score and EXP-01 charters for the top risk classes of this diff | Keep a per-app YAML or markdown ledger, and never close a bug without SUITE-05 evidence and a FAMILIAR_PROBLEMS entry | Major |
|
|
351
|
+
|
|
352
|
+
## Exit criteria
|
|
353
|
+
|
|
354
|
+
All of the following must be met before asking the owner for the submission go. Red or skipped rows are attached to the "not tested" list.
|
|
355
|
+
|
|
356
|
+
1. release_gate printed PASS on the exact build attached in ASC, the build number and GitCommit match the gate record, and the archive came from a clean tree (PLAN-05, BUILD-06, BUILD-11).
|
|
357
|
+
2. Smoke and the full plan are green on the Release build on the USB iPhone without a debugger. The executed count is above 0 and at least the last release's, the destination is the real device, and there is no unexplained FLAKY (FUNC-01, BUILD-01, SUITE-01, SUITE-02).
|
|
358
|
+
3. Across every device run of this build there are 0 new crash, watchdog, jetsam, or termination reason logs naming the app or an extension, and 0 runtime checker issues (STAB-01, STAB-05, PERF-04).
|
|
359
|
+
3a. The mutation gate is green: zero surviving and zero uncovered mutants on the logic lines changed since the last release (SUITE-08). Lines that no test checks show up here even when every test is green.
|
|
360
|
+
4. Fresh install, upgrade over N-1 (N-2 when the storage format changed), App Store N-1 → TestFlight N, and delete-and-reinstall pass while keeping seeded data, and no contract identifier changed (BUILD-03 to 05, BUILD-09).
|
|
361
|
+
5. There are 0 open S1 and S2 bugs, and every S3 and S4 that ships is in the release notes (DEF-01).
|
|
362
|
+
6. The accessibility audit has 0 unexplained issues in every screen state, and the Dynamic Type xs to AX5, ko and en, and visual settings sweeps were reviewed with no clipping or overlap (A11Y-01 to 04, L10N-01).
|
|
363
|
+
7. Performance is within budget: launch median within 15% of the last release, hitch ratio under 10 ms/s, memory growth within headroom, no app-owned leaks (PERF-01 to 03).
|
|
364
|
+
8. For CloudKit apps, the production schema has every type, field, and index the build uses, the pending deploy does not break the shipped version, GRANTs are correct, and the production round trip on the TestFlight build passed (CK-01 to 06).
|
|
365
|
+
9. Feeds, backend, AASA, links, and API keys were green within the last hour and look able to last through review, and every installed app version decodes the current payload (FEED-01, FEED-05, ENTRY-02, SUB-02).
|
|
366
|
+
10. `release_checks.py` and the static submission checks are green: no placeholders, per-bundle privacy manifests, SDK manifests, purpose strings, no IPv4 literals, background modes, export compliance, processingState VALID, no ITMS mail for this build (SUB-01 to 04, SUB-16 to 18, NET-05, PERM-04).
|
|
367
|
+
11. The privacy label, privacy policy, and data flow list agree, the owner re-checked the ASC App Privacy answers if the list changed, and the external terms gates (SUB-26, SUB-27) are not BLOCK (SUB-05, SUB-06, CK-07).
|
|
368
|
+
12. The review notes are within 4,000 UTF-8 bytes and cover the foreign reviewer path, What's New covers every change, screenshots match this build, the metadata lint passes, WeatherKit and external source attribution is on every screen, and the age rating answers are correct (SUB-08 to 15).
|
|
369
|
+
13. The TestFlight soak (2 or more days of real use for feature releases) shows crashCount 0 with no open feedback on the candidate, and the field checklist and the owner's exploratory session are done or on "not tested" (FIELD-01 to 03, EXP-03).
|
|
370
|
+
14. Manual release and phased release are configured, and the pause rules with a day-30 decision point, the rollout watch job, and the fix-forward runbook are ready (REL-01, REL-02, REL-06).
|
|
371
|
+
15. The "not tested" list and every red or skipped row are attached, and the owner accepted each remaining risk in their own words (PLAN-04).
|
|
372
|
+
|
|
373
|
+
## What only a human can do
|
|
374
|
+
|
|
375
|
+
- **One-time setup.** Turn on Developer Mode on the test iPhone, add the Korean 2-set keyboard (for a Korean-market app), place widgets on the home and Lock Screens, turn on Hang Detection, and sign in with a test-only Apple ID (TestFlight, App Store, iCloud). Also prepare a second iCloud account or device, a Screen Time restricted state, the Mac's NAT64 Wi-Fi, sudoers entries for `rvictl`, `tcpdump`, and `swcutil`, and the APNs .p8 for remote push apps.
|
|
376
|
+
- **30 seconds per release.** Cold launch the candidate or the TestFlight install once from the home screen with the cable unplugged, because it is unconfirmed whether a devicectl launch arms the watchdog.
|
|
377
|
+
- **A phone call and a lock per release.** While the agent holds the app mid-vote, call the test iPhone and lock and unlock it (INT-05).
|
|
378
|
+
- **10 to 20 minutes of exploration per release.** Use the TestFlight build with the agent's charter and contact sheet (EXP-03). On a major release, listen through one VoiceOver task flow (A11Y-03).
|
|
379
|
+
- **1 to 3 days of TestFlight field use.** Watch the Wi-Fi to cellular handoff on a walk, outdoor readability, real push and morning briefing times, the notification pile-up after Focus mode, and universal links tapped in Messages (FIELD-01, FIELD-03).
|
|
380
|
+
- **Irreversible buttons.** Read the agent's diff and press Deploy Schema Changes in CloudKit Console, or give the agent an explicit OK (CK-01). Approve the pause rules once, and give a go every time for Submit, the release request, raising minimum_version, and an expedited review submission.
|
|
381
|
+
- **Judgment.** Choose the ASC App Privacy answers when the data flow diff changes, and make legal judgments such as the Accessibility Nutrition Label, age rating changes, DSA and agreements, the Location Information Act, export cryptography, and external terms. The impact score of new capabilities in the risk list, the empty expected values in decision tables, and the remaining risk on "not tested" are also the owner's call.
|
|
382
|
+
- **Only when a feature or the storage format changes.** Run the available path on a phone that can use Apple Intelligence (for example the owner's own phone), and when the keychain or storage format changes, restore from another device's backup and try it.
|
|
383
|
+
|
|
384
|
+
## Sources
|
|
385
|
+
|
|
386
|
+
Dates are the document's publish or revision date; where there is none, the date checked (2026-10-28).
|
|
387
|
+
|
|
388
|
+
- App Review Guidelines, https://developer.apple.com/app-store/review/guidelines/ (2026-10-16)
|
|
389
|
+
- App Review, https://developer.apple.com/distribute/app-review/ (checked 2026-10-28)
|
|
390
|
+
- Complete review, https://developer.apple.com/help/app-review/before-submitting-for-review/complete-review (2026-10-05)
|
|
391
|
+
- Test compatible devices, https://developer.apple.com/help/app-review/before-submitting-for-review/test-compatible-devices (2026-10-05)
|
|
392
|
+
- 5.1.1(ii) Purpose strings, https://developer.apple.com/help/app-review/guideline-reference/5-1-1-purpose-strings (2026-10-05)
|
|
393
|
+
- 4.1 Prevent copycat rejection, https://developer.apple.com/help/app-review/guideline-reference/4-1-prevent-copycat-rejection (checked 2026-10-28)
|
|
394
|
+
- Platform version information, https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information (2026-10-06)
|
|
395
|
+
- Screenshot specifications, https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications (2026-10-15)
|
|
396
|
+
- Age ratings values and definitions, https://developer.apple.com/help/app-store-connect/reference/app-information/age-ratings-values-and-definitions (2026-10-23)
|
|
397
|
+
- App privacy details, https://developer.apple.com/app-store/app-privacy-details/ (2026-10-08)
|
|
398
|
+
- Describing use of required reason API, https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api (checked 2026-10-28)
|
|
399
|
+
- Third-party SDK requirements, https://developer.apple.com/support/third-party-SDK-requirements/ (2026-10-23)
|
|
400
|
+
- Upcoming requirements, https://developer.apple.com/news/upcoming-requirements/ (checked 2026-10-28)
|
|
401
|
+
- Release a version update in phases, https://developer.apple.com/help/app-store-connect/update-your-app/release-a-version-update-in-phases/ (checked 2026-10-28)
|
|
402
|
+
- TestFlight overview, https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview (checked 2026-10-28)
|
|
403
|
+
- EU DSA trader requirements, https://developer.apple.com/help/app-store-connect/manage-compliance-information/manage-european-union-digital-services-act-trader-requirements/ (checked 2026-10-28)
|
|
404
|
+
- Age assurance, https://developer.apple.com/support/age-assurance/ (checked 2026-10-28), announcement https://developer.apple.com/news/?id=sg176nne (2026-06-03)
|
|
405
|
+
- ASC API diagnosticSignatures, https://developer.apple.com/documentation/appstoreconnectapi/get-v1-builds-_id_-diagnosticsignatures (checked 2026-10-28)
|
|
406
|
+
- Testing a release build, https://developer.apple.com/documentation/xcode/testing-a-release-build (checked 2026-10-28)
|
|
407
|
+
- Organizing tests into test plans, https://developer.apple.com/documentation/xcode/organizing-tests-to-improve-feedback (checked 2026-10-28)
|
|
408
|
+
- Performing accessibility audits, https://developer.apple.com/documentation/accessibility/performing-accessibility-audits-for-your-app (checked 2026-10-28)
|
|
409
|
+
- Voice Control evaluation criteria, https://developer.apple.com/help/app-store-connect/manage-app-accessibility/voice-control-evaluation-criteria (checked 2026-10-28)
|
|
410
|
+
- Addressing watchdog terminations, https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations, SIGKILL, https://developer.apple.com/documentation/xcode/sigkill (checked 2026-10-28)
|
|
411
|
+
- Keeping a widget up to date, https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date, accented rendering, https://developer.apple.com/documentation/widgetkit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass (checked 2026-10-28)
|
|
412
|
+
- TN3155 Debugging universal links, https://developer.apple.com/documentation/technotes/tn3155-debugging-universal-links (2025-05-15)
|
|
413
|
+
- TN3187 Migrating to the UIKit scene-based life cycle, https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle (checked 2026-10-28)
|
|
414
|
+
- CloudKit text-based schema, https://developer.apple.com/documentation/cloudkit/integrating-a-text-based-schema-into-your-workflow, Using cktool, https://developer.apple.com/icloud/ck-tool/ (checked 2026-10-28)
|
|
415
|
+
- WeatherKit get started, https://developer.apple.com/weatherkit/get-started/ (checked 2026-10-28)
|
|
416
|
+
- HIG Privacy, https://developer.apple.com/design/human-interface-guidelines/privacy, HIG Maps, https://developer.apple.com/design/human-interface-guidelines/maps (checked 2026-10-28)
|
|
417
|
+
- Local tool help: `xcodebuild -help`, `man 5 xcodebuild.xctestrun`, devicectl 651.13.5, `cktool --help`, `XCUIVoiceOverService.h` (Xcode 27.1, checked 2026-10-28)
|
|
418
|
+
- Google Maps Platform Service Specific Terms, https://cloud.google.com/maps-platform/terms/maps-service-terms (2026-10-15), Places policies, https://developers.google.com/maps/documentation/places/web-service/policies (2026-10-23)
|
|
419
|
+
- Kakao DevTalk Local API storage question, https://devtalk.kakao.com/t/local-api/151263 (2026-08-20)
|
|
420
|
+
- Location Information Act Article 9-2 (Korea), https://casenote.kr/법령/위치정보의_보호_및_이용_등에_관한_법률/제9조의2 (in force 2025-10-01), Personal Information Protection Act Article 30 (Korea), https://casenote.kr/법령/개인정보_보호법/제30조 (in force 2023-09-15)
|
|
421
|
+
- OWASP MASVS v2.1.0, https://mas.owasp.org/MASVS/ (2024-01-18)
|
|
422
|
+
- ISTQB CTFL v4.0.1, https://www.istqb.org/wp-content/uploads/2024/11/ISTQB_CTFL_Syllabus_v4.0.1.pdf (2024-09-15), CT-MAT, https://www.istqb.org/wp-content/uploads/2024/11/ISTQB-CT-MAT_Syllabus_v1.0_2019.pdf (2019-05-03)
|
|
423
|
+
- J. Bach, HTSM, https://developsense.com/htsm (2024-02-08), M. Bolton, FEW HICCUPPS, https://developsense.com/blog/2012/07/few-hiccupps (2012-07-23)
|
|
424
|
+
- J. Kohl, I SLICED UP FUN, http://www.kohl.ca/articles/ISLICEDUPFUN.pdf (2010-10), Session-Based Test Management, https://www.satisfice.com/download/session-based-test-management (2000)
|
|
425
|
+
- Test Heuristics Cheat Sheet, https://www.ministryoftesting.com/articles/test-heuristics-cheat-sheet (2022-03-31)
|
|
426
|
+
- Nielsen, 10 usability heuristics, https://www.nngroup.com/articles/ten-usability-heuristics/ (reviewed 2024-01-30)
|