From b0b4b03f5dedaede00132095a5dd0f333c2bb1ff Mon Sep 17 00:00:00 2001 From: Charles Wiltgen Date: Sat, 19 Sep 2026 15:52:22 -0700 Subject: [PATCH] chore: rebuild the Codex, Cursor and MCP variants Regenerated from the skill, agent, command and hook changes in the preceding commits. --- axiom-codex/hooks/session-start.py | 2 +- .../skills/axiom-analyze-triage/SKILL.md | 6 +- .../skills/localization-research-ref.md | 22 +- .../axiom-integration/skills/localization.md | 73 + .../skills/axiom-swiftui/skills/iphone-duo.md | 51 +- .../skills/axiom-test-simulator/SKILL.md | 22 +- .../axiom-tools/skills/device-control-ref.md | 51 +- .../skills/axiom-tools/skills/xcui-ref.md | 37 +- .../axiom-uikit/skills/uikit-modernization.md | 1 + .../skills/axiom-xcode-mcp/skills/axe-ref.md | 62 +- axiom-cursor/agents/simulator-tester.md | 22 +- axiom-cursor/agents/triage-analyzer.md | 6 +- axiom-cursor/reports/inventory-sha256.json | 38 +- .../skills/localization-research-ref.md | 22 +- .../axiom-integration/skills/localization.md | 73 + .../skills/axiom-swiftui/skills/iphone-duo.md | 55 +- .../axiom-tools/skills/device-control-ref.md | 51 +- .../skills/axiom-tools/skills/xcui-ref.md | 37 +- .../axiom-uikit/skills/uikit-modernization.md | 1 + .../skills/axiom-xcode-mcp/skills/axe-ref.md | 62 +- axiom-mcp/dist/bundle-stats.json | 12 +- axiom-mcp/dist/bundle.json | 4779 ++++++++++++----- 22 files changed, 3928 insertions(+), 1557 deletions(-) diff --git a/axiom-codex/hooks/session-start.py b/axiom-codex/hooks/session-start.py index 35f881ff..c2eadb4d 100644 --- a/axiom-codex/hooks/session-start.py +++ b/axiom-codex/hooks/session-start.py @@ -164,7 +164,7 @@ try: --- -**xcui** (scriptable sim UI & accessibility testing): Available at `{xcui_path}`. Drives the simulator via AXe + simctl. Run `xcui doctor` first (verifies AXe; `--install` adds it via brew). Key verbs: `xcui wait --for-element `, `xcui assert --id --label … --trait … --single`, `xcui a11y set --toggle --value --app `, `xcui dialog accept|dismiss` (or `pregrant …`), `xcui voiceover traverse|assert --sequence `. For taps use `axe tap --id ` directly. Workflow: `axiom-tools` (skills/xcui-ref.md). Command: `/axiom:ui`.""" +**xcui** (scriptable sim UI & accessibility testing): Available at `{xcui_path}`. Drives the simulator via AXe + simctl. Run `xcui doctor` first (verifies AXe; `--install` adds it via brew). Key verbs: `xcui wait --for-element `, `xcui assert --id --label … --trait … --single`, `xcui a11y set --toggle --value --app `, `xcui dialog accept|dismiss` (or `pregrant …`), `xcui voiceover traverse|assert --sequence `. For taps use `xcui tap --id ` — it sends a physical touch; bare `axe tap` needs `--tap-style physical` or SwiftUI controls ignore it while still reporting success. Workflow: `axiom-tools` (skills/xcui-ref.md). Command: `/axiom:ui`.""" except OSError: pass diff --git a/axiom-codex/skills/axiom-analyze-triage/SKILL.md b/axiom-codex/skills/axiom-analyze-triage/SKILL.md index 1c39c04a..d7c42396 100644 --- a/axiom-codex/skills/axiom-analyze-triage/SKILL.md +++ b/axiom-codex/skills/axiom-analyze-triage/SKILL.md @@ -16,7 +16,9 @@ You are an expert at corpus-level production crash and hang triage. You fetch gr ## Single-Crash Escape Hatch -If the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file `xcsym crash` pipeline with dSYM discovery and symbolication. This agent is for corpus triage from Sentry / ASC only. +If the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file `xcsym crash` pipeline with dSYM discovery and symbolication. + +If the corpus is the **local Xcode Organizer one** — `~/Library/Developer/Xcode/Products//Crashes/Points/*.xccrashpoint` — read `axiom-shipping (skills/testflight-triage.md)`, "The On-Disk Organizer Corpus". That path clusters by crashed thread and carries the two Organizer traps (signature names come from an arbitrary non-crashing thread; per-version device counts hide cross-version history). This agent is for corpus triage from Sentry / ASC only. ## Workflow @@ -162,4 +164,4 @@ Explicit command: `/axiom:triage [sentry|asc]` ## Scope -Fetches unresolved issues, normalizes them, runs `xcsym triage` to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead. +Fetches unresolved issues, normalizes them, runs `xcsym triage` to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead; for the **local Organizer corpus** of `.xccrashpoint` bundles, use `axiom-shipping (skills/testflight-triage.md)`. diff --git a/axiom-codex/skills/axiom-integration/skills/localization-research-ref.md b/axiom-codex/skills/axiom-integration/skills/localization-research-ref.md index 43325223..c352c798 100644 --- a/axiom-codex/skills/axiom-integration/skills/localization-research-ref.md +++ b/axiom-codex/skills/axiom-integration/skills/localization-research-ref.md @@ -38,9 +38,29 @@ Do NOT use this skill for: Users expect "Shuffle," "Up Next," "Now Playing," "Smart Playlist" to mean exactly what they mean in Apple Music. Diverging from Apple's canonical translations feels wrong in every language — even if your translation is technically correct. +### Apple's own translations, on disk (most exact) + +Every installed simulator runtime ships Apple's shipped `.strings` for its bundled apps — the same text users read in Music, Mail, and Settings. `.strings` there are binary plists, so read them with `plutil`: + +```bash +RT=$(xcrun simctl list runtimes -j | python3 -c 'import json,sys; print([r["runtimeRoot"] for r in json.load(sys.stdin)["runtimes"] if r["platform"]=="iOS"][-1])') +ls "$RT/Applications/Music.app" | grep lproj # 56 locales in the 27.0 and 27.1 runtimes +plutil -p "$RT/Applications/Music.app/fr.lproj/Localizable.strings" | grep -i '"Songs"' +``` + +Music.app's keys are the English text, so grep the key and read the value: `Songs` is `Morceaux` in French and `노래` in Korean; `Shuffle` is `Aléatoire` and `임의 재생`. Some apps ship no `en.lproj` for exactly that reason — the key is the English. + +The same runtime holds hundreds of **`AppShortcuts.strings`** files (753 in the iOS 27.0 runtime), which are Apple's own registered Siri phrases — the precedent to follow for phrase *structure*, not just vocabulary: + +```bash +find "$RT" -name AppShortcuts.strings | head +``` + +Use this first when you need the exact form of a UI string. Use the Support pages below when you need what a user *says* out loud, which is a different register — the button may be `Lire` while the Siri verb is `mets`. + ### Primary Sanity Check (Authoritative) -**Apple Support multi-locale pages** — the authoritative source. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare: +**Apple Support multi-locale pages** — authoritative for spoken and user-facing phrasing. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare: | Locale | URL pattern | |--------|-------------| diff --git a/axiom-codex/skills/axiom-integration/skills/localization.md b/axiom-codex/skills/axiom-integration/skills/localization.md index 76389e28..962d31a9 100644 --- a/axiom-codex/skills/axiom-integration/skills/localization.md +++ b/axiom-codex/skills/axiom-integration/skills/localization.md @@ -90,6 +90,57 @@ Each entry has: } ``` +### Two key styles — and what a tool must read + +An entry's key is either the English source text itself or a symbolic identifier. The first entry below has the English as its key; the second uses a symbolic key and carries the English in an explicit source unit. + +```json +"Songs" : { + + }, +"quality.high" : { + "comment" : "Playback quality: 256 kbps", + "extractionState" : "extracted_with_value", + "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "High" } } } +} +``` + +`String(localized: "Songs")` produces the first; `String(localized: "quality.high", defaultValue: "High")` produces the second, with `extracted_with_value` and an explicit source unit. So **the source string is `localizations[sourceLanguage]` if present, else the key** — a tool has to handle both, not pick one. (`sourceLanguage` is a top-level field and is not always `en`: in a catalog whose source is German the unit lands under `de`, so a tool that hardcodes `en` reads a translation as if it were the source.) A tool that reads only the source localization skips every key-as-English entry, and a search over keys alone misses every symbolic one. + +### Writing a catalog from a tool + +Xcode has one byte layout, and a generic JSON writer rewrites the whole file — measured on a real 970-key catalog, `json.dump(..., indent=2)` produced a **26,531-line diff** that buried the actual translation. The layout: + +| Rule | Value | +|---|---| +| Indent | 2 spaces | +| Key separator | `" : "` — a space *before* the colon | +| Encoding | literal UTF-8, never `\u` escapes | +| Trailing newline | none | +| Empty entry | `{`, a blank line, then `}` at the entry's own indent | +| Keys inside an entry | sorted by code point | +| Keys of the top-level `strings` map | two writers, two orders — see below | + +**The two Apple writers order the top-level keys differently.** `xcstringstool` sorts by code point. The Xcode editor's order matches Foundation's `localizedStandardCompare`, which is punctuation-aware: measured across two editor-written revisions of the same 1,086- and 1,447-key catalog, that comparison had 0 violations while plain code-point order had 139 and 212. So running `xcstringstool sync` over an editor-maintained catalog re-sorts every key — a 16,005-line diff on a file whose meaning did not change. Match whichever order the file already uses. + +**`xcstringstool` ships with Xcode** and is the headless path in and out of a catalog. It is not on `PATH`; run it through `xcrun`: + +```bash +xcrun xcstringstool extract --SwiftUI --modern-localizable-strings -o /tmp/sd $(find Sources -name '*.swift') +xcrun xcstringstool sync Localizable.xcstrings --stringsdata /tmp/sd/*.stringsdata --skip-marking-strings-stale +xcrun xcstringstool print Localizable.xcstrings # every key +``` + +(`Sources/**/*.swift` is not a substitute for that `find`: macOS ships bash 3.2, which has no `globstar`, so `**` silently matches one directory level.) + +**`sync` prunes.** It is not an additive merge: every entry missing from the `.stringsdata` is marked `extractionState: stale` if it has translations and **deleted outright if it has none** — exit 0, no warning. Measured on a two-entry catalog: the untranslated entry vanished and the translated one came back stale. `--skip-marking-strings-stale` is the guard, and with it a 1,447-key catalog synced intact (122 untranslated entries all survived). + +That makes the filename matter twice over: `sync` matches a `.stringsdata` table to the catalog **by filename**, so `Localizable.xcstrings` takes the `Localizable` table. Point it at `Strings.xcstrings` and nothing matches — so nothing is added, and without the guard flag every entry is pruned or staled. + +`sync` re-emits the canonical layout only when it has something to write; a pure reformat with no source change is left exactly as it is, so it is not a repair tool for a bad write. + +To write translations — which `sync` does not do — parse, mutate only the entries you mean to touch, and re-emit in the layout above, ordering the top-level keys the way the file already orders them. In JavaScript, `JSON.parse` also hoists integer-like keys (`"30"`) to the front, so a parse/stringify round-trip reorders the file on its own; parse into an order-preserving structure instead. + ### Translation States Xcode tracks state for each translation: @@ -796,6 +847,28 @@ String(localized: "Confirm", comment: "Button to confirm delete action") **Impact**: "Confirm" could mean "verify" or "acknowledge" - context matters for accurate translation. +### Expecting a comment to separate two meanings + +A comment gives a translator context; it does **not** split a key. One English word used for two concepts is one entry with one translation, and the comments are merged: + +```swift +String(localized: "High", comment: "Energy level of a track") +String(localized: "High", comment: "Playback quality: 256 kbps") +``` + +```json +"High" : { "comment" : "Energy level of a track\nPlayback quality: 256 kbps" } +``` + +`xcstringstool sync` says so — `notice: Key "High" used with multiple comments: …` — and then the catalog ships whichever translation the translator picked for both. Give each meaning its own key and keep the English in a default value: + +```swift +String(localized: "energy.high", defaultValue: "High", comment: "Energy level of a track") +String(localized: "quality.high", defaultValue: "High", comment: "Playback quality: 256 kbps") +``` + +This bites hardest where a short word is reused across features — a picker label and a Siri phrase, say — because the wrong sense only shows up in the other feature's UI. Before renaming any enum title or button to a short common word, search the catalog for that exact key. + --- ## Troubleshooting diff --git a/axiom-codex/skills/axiom-swiftui/skills/iphone-duo.md b/axiom-codex/skills/axiom-swiftui/skills/iphone-duo.md index 842c42a4..279597fc 100644 --- a/axiom-codex/skills/axiom-swiftui/skills/iphone-duo.md +++ b/axiom-codex/skills/axiom-swiftui/skills/iphone-duo.md @@ -191,6 +191,35 @@ Built against the 27.1 SDK, navigation, toolbar, and tab bar items share one ver - Keyboard accessory bars stay on the keyboard. - The inner display in portrait keeps horizontal bars. +#### Host the bar in a system-managed container + +Items join the vertical bar only from inside one of the system-managed containers above. The same `.bottomBar` items on a view with none of them — in a bare `fullScreenCover`, say — render as a horizontal capsule along the bottom, while `toolbarVerticalEdge` still reads `.trailing` (measured on the 27.1 Duo simulator, closed). Adding a `NavigationStack` to a custom full-screen layer brings two side effects: + +- **The stack paints an opaque background** over whatever is behind it. Apply `.containerBackground(.clear, for: .navigation)` to the content *inside* the stack; applied to the `NavigationStack` itself it had no effect (both measured). +- **The stack gives its content the window's safe-area insets back**, even under an ancestor's `.ignoresSafeArea()` — measured trailing 84 pt and bottom 34 pt on the closed Duo, where the content outside the stack read zero. Re-apply `.ignoresSafeArea()` inside the stack if the layout depends on reading zero. + +```swift +struct PlayerCover: View { + var body: some View { + ZStack { + ArtworkBackground() + NavigationStack { + PlayerControls() + .ignoresSafeArea() + .toolbar(.hidden, for: .navigationBar) + .containerBackground(.clear, for: .navigation) // inside the stack, not on it + .toolbar { + ToolbarItem(placement: .bottomBar) { + Button("Shuffle", systemImage: "shuffle") { } + } + } + } + } + .ignoresSafeArea() + } +} +``` + #### Order items top to bottom 1. Back (automatic in a navigation container) or a custom close. SwiftUI: `.cancellationAction`. UIKit: a leading item, with `leftItemsSupplementBackButton` left `false` (the default). @@ -224,9 +253,16 @@ inboxItem.badge = .count(7) // iOS 26: a symbol-only item that still - Vertical bars have no scroll-edge effect but gain a background under Reduce Transparency — keep custom content legible either way. - A hero or background image extends under the vertical bar with `.backgroundExtensionEffect()` (SwiftUI) or `UIBackgroundExtensionView` (UIKit). +#### What your styling survives + +- **System items ignore your styling.** In the vertical bar a system `Toggle` or `Button` item ignores `.foregroundStyle` and `.opacity`; a `Toggle` shows "on" as a lit disc. `.contextMenu` on a system `Toggle` item does nothing (measured on the 27.1 Duo simulator). +- **A custom view keeps it (measured).** Opt the item in with `.axisBehavior(.verticalPreferred)` and its own `.foregroundStyle`, `.opacity`, and `.contextMenu` all work in the bar — the route for on/off/unavailable states the system rendering can't show. +- **`Menu(primaryAction:)` shows no on/off state** in the bar (reported, not re-measured). +- **The overflow menu shows each item's title and icon**, taken from its `Label`, but **no on/off state and no context menu** (reported). If state matters once an item overflows, put it in the title ("Repeat One"). + #### Plan for overflow -The outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up. A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar. +The outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up; `.visibilityPriority(.high)` works on custom items too (measured: of ten custom items in a seven-slot bar, the last two marked `.high` stayed and the three above them overflowed). A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar. #### When to turn vertical bars off @@ -236,6 +272,12 @@ A single-page, bottom-heavy layout like a calculator, or a sheet whose only item The inferred axis is usually right — a title-only item stays horizontal, an item with an image goes vertical. Override it per item when a custom view, a wide control, or a symbol↔text toggle needs a specific axis. All of these are iOS 27.1; below that, items keep whatever axis the system infers, and the knobs don't exist. A build against the 27.0 SDK or earlier never sees a vertical bar at all. +`toolbarVerticalEdge` (UIKit: `traitCollection.verticalBarEdge`) reports the system's *preferred* edge "regardless of whether a vertical bar is currently visible", and is nil (`.unspecified`) where the system never places one — hardware without a vertical bar, or a size class or orientation that doesn't use it (UIKit header doc). What the value does and doesn't tell you: + +- It is set before any toolbar item exists, and reads the same inside and outside a navigation container (measured). +- It is nil on the open inner display in portrait, half-folded included, and reads nil briefly during the opening transition (reported). Don't animate on a transient nil. +- A non-nil edge doesn't mean *your* items are vertical — outside a system-managed container they stay horizontal (Host the bar in a system-managed container). + ```swift // SwiftUI — iOS 27.1: per-item axis override, compression order, and the edge read @available(iOS 27.1, *) @@ -257,7 +299,7 @@ struct BarControls: View { } .toolbarVerticalCompressionBehavior(.prefersToolbarItems) // tab bar compresses first .overlay(alignment: .bottom) { - // .leading / .trailing while a bar is vertical; nil when items can't go vertical + // .leading / .trailing where the system places bars vertically; nil where it never does Text(edge == .trailing ? "Bar: trailing" : edge == .leading ? "Bar: leading" : "No vertical bar") .font(.caption) } @@ -530,6 +572,11 @@ Availability (`isAvailable`) and your on/off switch (`isEnabled`) are separate: ## Tooling and Testing - **Device Hub** — Xcode 27.1's Device Hub drives an iPhone Duo simulator with open, close, rotate, and fold controls (111461 0:56); Apple's overview notes the Duo simulator in Device Hub requires Xcode 27.1. The device type creates against the **iOS 27.1 runtime**; the 27.0 runtime rejects it (`Incompatible device`). `iPhone Fold` is a different product. +- **The 27.1 runtime is Duo-only.** Creating iPhone 17, 17 Pro, 17e, or iPad Pro on it fails with the same `Incompatible device` (SimError 403), so keep the 27.0 runtime for every other device (measured with Xcode 27.1). +- **Poses are Device Hub-only.** No `simctl` or `devicectl` command opens, closes, or folds the simulator. `devicectl device motion hinge-angle` only *reads* the hinge, and `devicectl device orientation set` prints success and changes nothing on the Duo (measured closed; reported open). Check the window size after every pose change: closing from open-landscape has been reported landing in closed-portrait one time and closed-landscape another. +- **Screenshots default to the inner display**, which is black while the device is closed — for `simctl io … screenshot` and `devicectl device capture` alike. Capture the outer display by name: axiom-tools (skills/device-control-ref.md, Display masks and multi-display devices). +- **Name closed poses by the hinge**, never by "turned left/right". Closed and upright, the hinge is the left edge, the camera and status bar sit top-right, and the vertical bar runs down the trailing edge (`toolbarVerticalEdge == .trailing`, measured). With the hinge along the bottom the camera is top-left and the bar moves to the leading edge; hinge along the top puts the camera bottom-right and the bar trailing (reported). The bar follows the camera's edge. +- **Synthetic taps** — send a physical touch (`xcui tap` does; bare `axe tap` needs `--tap-style physical`), or the bar's items ignore the tap while it reports ✓: axiom-tools (skills/xcui-ref.md, Tap styles). The bar's overflow (⋯) button did not open under any tap style or a 0.2–1.0 s hold in the closed pose (measured); test overflowed actions in a pose with room for them. - **Simulator gaps** — per the Xcode 27.1 beta release notes, StandBy is unavailable in the iPhone Duo Simulator runtime, and running and debugging most app extensions is unavailable there. - **App Resizability** — Xcode's app-modernization agent skill, renamed "App Resizability", now covers SwiftUI and iPhone Duo (111461 9:15). See axiom-uikit (skills/uikit-modernization.md). diff --git a/axiom-codex/skills/axiom-test-simulator/SKILL.md b/axiom-codex/skills/axiom-test-simulator/SKILL.md index 4a1bec40..72a8af1b 100644 --- a/axiom-codex/skills/axiom-test-simulator/SKILL.md +++ b/axiom-codex/skills/axiom-test-simulator/SKILL.md @@ -34,13 +34,20 @@ xcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(. # Check booted simulators xcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.state == "Booted") | {name, udid}' -# Get specific device UDID for commands -UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) +# Get specific device UDID for commands — only when exactly ONE is booted. +# With several booted (other sessions often keep their own), choose by name from the +# list above; `head -1` silently drives whichever sorts first, and its taps still print ✓. +BOOTED=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid') +if [ "$(printf '%s\n' "$BOOTED" | grep -c .)" = 1 ]; then + UDID=$BOOTED +else + echo "Several simulators are booted — set UDID to the one you mean from the list above" +fi # Boot if needed (get UDID first, then boot) xcrun simctl boot "iPhone 16 Pro" -# Preflight AXe + booted sim with xcui doctor (AXe enables real HID tap/swipe/type/describe-ui) +# Preflight AXe + booted sim with xcui doctor (AXe enables tap/swipe/type/describe-ui) if command -v axe &> /dev/null; then echo "AXe available - UI automation enabled (tap, swipe, type, describe-ui)" AXE_AVAILABLE=true @@ -238,7 +245,8 @@ xcui doctor --install # installs cameroncooke/axe/axe via brew if missing # Discover UI elements first (get accessibility identifiers) axe describe-ui --udid $UDID -# Tap by accessibility identifier (RECOMMENDED - stable) +# Tap by accessibility identifier (RECOMMENDED - stable). xcui sends a physical +# touch; bare `axe tap` needs --tap-style physical or SwiftUI controls ignore it xcui tap --id "loginButton" --udid $UDID # Tap by label @@ -247,8 +255,8 @@ xcui tap --label "Submit" --udid $UDID # Tap at coordinates (less stable) xcui tap -x 200 -y 400 --udid $UDID -# Long press -xcui tap -x 200 -y 400 --duration 1.0 --udid $UDID +# Long press (tap has no hold option; 1.2 s opens a .contextMenu) +xcui touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID # Gesture presets xcui gesture scroll-down --udid $UDID # Scroll content down @@ -303,7 +311,7 @@ xcui a11y set --toggle reduce-transparency --value on --app com.example.App xcui a11y set --toggle dynamic-type --value accessibility-extra-large ``` -Supported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`. For taps, use `xcui tap --id ` (forwards to AXe's real HID touch). Full reference: `axiom-tools (skills/xcui-ref.md)`. +Supported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`, `voiceover`. For taps, use `xcui tap --id ` (a physical touch down/up). A tap prints ✓ whether or not anything happened, so assert the result with `xcui wait`/`xcui assert` rather than trusting the tap's output. With more than one simulator booted, pass `--udid` on every xcui command — xcui refuses to guess. Full reference: `axiom-tools (skills/xcui-ref.md)`. ### 16. Network Conditioning (low-bitrate / latency / loss) diff --git a/axiom-codex/skills/axiom-tools/skills/device-control-ref.md b/axiom-codex/skills/axiom-tools/skills/device-control-ref.md index 981116d2..06030570 100644 --- a/axiom-codex/skills/axiom-tools/skills/device-control-ref.md +++ b/axiom-codex/skills/axiom-tools/skills/device-control-ref.md @@ -110,7 +110,8 @@ CI order is unchanged at the front: simctl or xcodebuild boots the sim → devic |------------|--------------|-----| | `device info displays` | works (verified) | bounds, pointScale, nativeSize, `framebufferMaskIdentifier` (exact JSON keys) | | `device capture screenshot` / `screen-record` | works (verified) | PNG / H.264 `.mp4` capture, sim or device — see Screen capture below | -| `device orientation get` (also `set`, `rotate`) | works (`get` verified) | orientation without entering the app | +| `device orientation set` / `get` (also `rotate`) | works (verified) — **except iPhone Duo**, see Rotating a simulator | rotate a sim headlessly; simctl has no rotate verb | +| `device motion hinge-angle` | works, read-only | streams the hinge angle (0° closed, 180° open) for 60 s by default; nothing in devicectl or simctl sets it | | `device settings biometrics [--enable\|--disable]` | works (verified) | enroll / unenroll Face ID / Touch ID | | `device simulate biometrics --success\|--failure` | works (verified) | drive a match / no-match | | `device settings appearance --mode light\|dark` | works (verified) | force Dark/Light; also `--look-and-feel clear\|tinted`, text size, contrast | @@ -143,6 +144,21 @@ ERROR: The capability "Get Lock State" is not supported by this device. `info lockState` is confirmed device-only; `info files`, `copy`, and `profile *` are reported device-only on simulators. In CI, treat `CoreDeviceError 1001` as "skip on simulator", not a failure. +### Rotating a simulator + +`simctl` has no rotate verb, and keystroke automation of the GUI is no substitute — it reported success while rotating nothing. Rotate from the host with devicectl and verify by screenshot **dimensions**: + +```bash +xcrun devicectl device orientation set -d "$SIM" landscapeLeft # portrait | portraitUpsideDown | landscapeLeft | landscapeRight +xcrun simctl io "$SIM" screenshot rotated.png # width and height must swap +xcrun devicectl device orientation set -d "$SIM" portrait +``` + +Verified 2026-09-19: iPhone 17 (iOS 27.0) 1206×2622 → 2622×1206, iPad Pro 11-inch (iOS 27.0) 1668×2420 → 2420×1668, and the app received the new size both times. + +- **iPhone Duo ignores it.** `set` prints `New Device Orientation: landscapeLeft`, `get` still reads `portrait`, and nothing rotates — measured closed, and reported open on the inner display too. Duo poses change only through Device Hub's pose controls: axiom-swiftui (skills/iphone-duo.md, Tooling and Testing). +- **On iPad, only the host can rotate.** An app's own `requestGeometryUpdate` is refused on iPad under the windowing model (`UISceneErrorDomain` 101; axiom-uikit (skills/uikit-modernization.md)), so a test that needs landscape should rotate from the harness with the command above, not from inside the app. + ## Screen capture — screenshot & video `devicectl device capture` is the **unified** capture path: one `-d ` selector across @@ -162,7 +178,7 @@ xcrun devicectl device capture screen-record -d --destination clip.mp4 -- | Flag | screenshot | screen-record | Notes | |------|------------|---------------|-------| | `--destination` | `.png` only | `.mp4` only | wrong extension is a hard error, not a coercion | -| `--display-unique-id` | yes | yes | pick from `device info displays`; omit = primary display | +| `--display-unique-id` | yes | yes | the `uniqueId` from `device info displays`. Omitted, the capture is **not** always the primary display — on iPhone Duo it is the inner display, even while closed and dark | | `--codec` | — | `h264` (default), `hevc` | | | `--mask-policy` | — | `ignored` (default), `premultipliedAlpha`, `black` | bezel mask for non-rectangular displays | | `--duration ` | — | auto-stop after N seconds | omit = record until SIGINT | @@ -177,10 +193,33 @@ Reach for these only when devicectl capture doesn't fit — none reach a physica | Tool | Use | Watch out | |------|-----|-----------| -| `simctl io screenshot [--type png] ` | sim PNG; `-` writes to stdout | sim only | -| `simctl io recordVideo [--codec h264\|hevc] [--mask ignored\|alpha\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only | +| `simctl io screenshot [--type png] [--display ] [--mask ignored\|alpha\|black] ` | sim PNG; `-` writes to stdout | sim only; `--display` takes a port UUID from `simctl io enumerate` (not devicectl's `uniqueId`) | +| `simctl io recordVideo [--codec h264\|hevc] [--mask ignored\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only. `--mask alpha` is accepted but unsupported for video — it renders black | | `axe record-video --output f.mp4` / `axe stream-video` | sim video / live preview stream (mjpeg, jpeg, ffmpeg, bgra) | sim only; `record-video` stops on Ctrl+C — see `axiom-xcode-mcp (skills/axe-ref.md)` | +### Display masks and multi-display devices + +For non-rectangular displays, `simctl io … screenshot --mask` chooses what the corners hold: + +| `--mask` | Corners | Use it to see | +|---|---|---| +| `alpha` | transparent (premultiplied alpha) | what the user sees on the glass | +| `ignored` | the full framebuffer rectangle | what a device screenshot captures — anything a view leaves under a corner lands in it | +| `black` | black, no alpha channel | a flat image for tools that drop alpha | + +Compare `alpha` and `ignored` when judging content near a corner. The alpha channel is also a measurement: on iPhone Duo's outer display the fully transparent run along the top and bottom rows is 18 px at the left corners and 185 px at the right (iOS 27.1 simulator). + +**A device with two displays needs the display named.** iPhone Duo's default capture — `simctl io` and `devicectl device capture` alike — is the inner display (2007×2853 px), which is black while the device is closed. Capture the outer display (1398×2034 px) explicitly: + +```bash +xcrun simctl io "$SIM" enumerate # the outer display's port is the one 1398 wide +xcrun simctl io "$SIM" screenshot --display= outer.png +xcrun devicectl device info displays -d "$SIM" # outer is "LCD", inner "LCD-1"; shows which is active +xcrun devicectl device capture screenshot -d "$SIM" --display-unique-id --destination outer.png +``` + +The two tools use different identifiers for the same display: simctl wants the **port UUID** from `enumerate`, devicectl the **`uniqueId`** from `info displays`. Guessing names fails — simctl rejects `--display=LCD-2`, and `2`, `4`, and `external` are accepted but hang without a frame. + ## Status bar for screenshots A shipping screenshot needs a clean status bar — 9:41, full bars, full battery. On a simulator @@ -384,7 +423,9 @@ trying to kill is still running. Confirm against the process, not the exit code: ```bash killall -9 Simulator DeviceHub pgrep -l Simulator DeviceHub # must print NOTHING -``` CarPlay simulation moved with it, into DeviceKit's +``` + +CarPlay simulation moved with it, into DeviceKit's `CarPlaySimulator.devicekitplugin`; the Xcode 26 `defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES` key does not exist anywhere in the 27 toolchain. diff --git a/axiom-codex/skills/axiom-tools/skills/xcui-ref.md b/axiom-codex/skills/axiom-tools/skills/xcui-ref.md index 303d12ad..312efd80 100644 --- a/axiom-codex/skills/axiom-tools/skills/xcui-ref.md +++ b/axiom-codex/skills/axiom-tools/skills/xcui-ref.md @@ -1,6 +1,6 @@ # xcui Reference (Scriptable Simulator UI & Accessibility Testing) -xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself. +xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself. ## Invocation @@ -10,11 +10,13 @@ On **Codex, Pi, and MCP installs there is no bundled binary**: those install ski If it is absent, you have two working options and should say which you took: - Put it on PATH — clone the repo and symlink `.claude-plugin/plugins/axiom/bin/xcui` (see the Pi install guide). This is the only way to get `resize sweep`, `wait`, `assert`, `a11y`, `dialog`, and `voiceover`, which have no AXe equivalent. -- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards, and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided. +- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards — add `--tap-style physical` to every `axe tap` (see Tap styles) — and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided. ## Prerequisite: run `xcui doctor` -`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready; exit 2 = AXe missing or no booted sim (see `problems`/`next_steps` in the JSON). When several sims are booted, every verb targets the lowest UDID deterministically; `doctor` adds a `note` listing them, and `--udid ` (accepted by every verb, `doctor` included) targets a specific one. +`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready to drive; exit 2 = something would stop a device command — AXe missing, no booted sim, several booted with no `--udid`, or an AXe too old for the tap style xcui sends (AXe 1.7.0 added `--tap-style`). Read `problems`/`next_steps` in the JSON; `booted` lists every booted device, so a `--udid` can be picked from it without parsing prose. + +**More than one booted sim → pass `--udid` on every command.** Without it, every device verb refuses with exit 2 and lists each booted sim's UDID, name, and runtime, so the retry is one step. It refuses rather than guessing because a guess drives the wrong device while every tap still prints ✓. With one booted sim, `--udid` is optional. `doctor` doesn't refuse — it reports the devices in `booted` and fails the gate, leaving `booted_udid` empty because nothing may be targeted in that state. ## Subcommands @@ -27,7 +29,7 @@ If it is absent, you have two working options and should say which you took: - `reduce-transparency` — `defaults write com.apple.Accessibility ReduceTransparencyEnabled`; needs relaunch (pass `--app`). - `voiceover` — `devicectl device settings voiceover --enable|--disable`; `--value` is `on`/`off`. Applies live; no relaunch. The only toggle that leaves the simctl/defaults world — simctl has no VoiceOver setter. Read the state back with `xcrun devicectl device info voiceover -d `. - `xcui a11y reset` — clear xcui-set overrides (delete the defaults keys, content_size → large, increase_contrast → disabled). -- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert. +- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button with a physical touch: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert. - `xcui dialog pregrant … [--udid ]` — grant permissions ahead of time via `simctl privacy … grant`, so the dialog never appears. Services are `simctl privacy` names (`camera`, `photos`, `location`, `microphone`, `contacts`, …). Prefer this over `accept` when you control the test setup — no alert means nothing to race. - `xcui voiceover traverse [--udid ]` — emit the **computed** VoiceOver announcement sequence: walk the a11y tree in focus order (top-to-bottom, leading-to-trailing) and render each focusable element as `label, value, trait` (plus `dimmed` when disabled). Output is a `sequence` JSON array. - `xcui voiceover assert --sequence [--udid ]` — compare the live announcement sequence to an expected one; the file may be a bare JSON string array **or** a saved `traverse` report (it round-trips). Reports every differing index (one entry per mismatched position, plus a length-mismatch note when counts differ); exit `1` on any mismatch. @@ -70,16 +72,37 @@ Three `devicectl` failures are separated, because they need different fixes: ## Input — `xcui tap`, not `axe tap` -Input verbs forward to AXe verbatim: same flags, same output, same exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling comes along automatically, so guidance can't drift out of it. +Input verbs forward to AXe: the flags you pass go through untouched, with the same output and exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling and the physical tap style come along automatically, so guidance can't drift out of them. -Forwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted. `xcui tap --help` shows AXe's own flags. +Forwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted and exactly one sim is booted. `xcui tap --help` shows AXe's own flags. ```bash -xcui tap --id loginButton --udid # real HID touch, not pointer-hover +xcui tap --id loginButton --udid # physical touch down/up xcui type "user@example.com" --udid +xcui touch -x 200 -y 400 --down --up --delay 1.2 --udid # long press — tap has no hold option axe describe-ui --udid # raw a11y tree (xcui assert/wait parse this) ``` +### Tap styles + +AXe's `tap` takes `--tap-style automatic|simulator|physical`. AXe's default, `automatic`, sends a physical touch only to switches and toggles and FBSimulator `tapAt` to everything else — and `tapAt` activated none of the targets below: + +| SwiftUI target | `automatic` | `simulator` | `physical` | +|---|---|---|---| +| `Toggle` / switch | fires (the one target `automatic` sends a physical touch to) | no effect | fires | +| `Button` | no effect | no effect | fires | +| `List` row (`NavigationLink`) | no effect | no effect | pushes | +| `Button` inside a `List` | no effect | no effect | fires | +| `Menu` | no effect | no effect | opens | +| Row of an open `Menu` | no effect | no effect | fires | +| Tab of a `TabView` (iPhone) | no effect | no effect | selects | + +The switch row is AXe's documented behavior for `automatic` ("physical touch for switches/toggles and simulator tap for other targets"); the rest was measured 2026-09-19 with Xcode 27.1 and AXe 1.8.0 on iPhone 17 (iOS 27.0) and iPhone Duo (iOS 27.1) simulators, where AXe printed `✓ … completed successfully` for every no-effect cell. So `xcui tap` adds `--tap-style physical` when you pass no style (pass one to override), and `xcui dialog` taps physically. Calling `axe tap` yourself? Add `--tap-style physical`. + +**Verify the effect, never the ✓.** A tap that changes nothing prints the same success line as one that works. After any tap that matters, confirm the result with `xcui wait --for-element`, `xcui assert`, or a screenshot. + +**Selectors tap the accessibility frame, not the pixels.** `--id` and `--label` resolve to the element's accessibility activation point. When a view's accessibility frame is skewed — a label hidden with `.fixedSize()` plus `.frame(width: 0).clipped()` is one measured case, where the tap landed about 27 pt from the visible control — the tap misses and still prints ✓. Screenshot, measure the visible center, and tap `-x/-y` in points, or fix the view so its frame matches what it draws. + > **Still calling `axe` directly?** `describe-ui`, `stream-video`, and `record-video` stay bare — the first is what xcui itself parses, and the two streaming verbs outlive any request timeout. Those are the only calls that need a `DEVELOPER_DIR=` prefix, and only when `xcui doctor` reports an `axe_developer_dir` (rare — AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided). ## Output & exit codes diff --git a/axiom-codex/skills/axiom-uikit/skills/uikit-modernization.md b/axiom-codex/skills/axiom-uikit/skills/uikit-modernization.md index 743e3c49..d14eff8e 100644 --- a/axiom-codex/skills/axiom-uikit/skills/uikit-modernization.md +++ b/axiom-codex/skills/axiom-uikit/skills/uikit-modernization.md @@ -147,6 +147,7 @@ You no longer own a fixed canvas — you express preferences the user and system windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 600) ``` - **Orientation lock** — a *preference*, not a guarantee, in resizable environments. Override `UIViewController.prefersInterfaceOrientationLocked` (returns `Bool`) and call `setNeedsUpdateOfPrefersInterfaceOrientationLocked()` when it changes; read the resolved state from `windowScene.effectiveGeometry.isInterfaceOrientationLocked` (iOS 26). +- **Requesting an orientation** — iPad refuses it under the windowing model. `windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))` calls its `errorHandler` with `UISceneErrorDomain` code 101, "The current windowing mode does not allow for programmatic changes to interface orientation", and the scene keeps its size. The error handler is the only signal, so always pass one. The same request rotated an iPhone scene (measured on iPad Pro 11-inch and iPhone 17, iOS 27.0 simulators). On iPad, lay out for the size you're given instead of forcing an orientation. - **Interactive vs settled resize** — `UIWindowSceneGeometry.isInteractivelyResizing` (iOS 26) is `true` while the user drags; throttle expensive work during the drag and settle when it clears. SwiftUI's equivalent is `.onInteractiveResizeChange(_:)` (see axiom-swiftui (skills/layout-ref.md)). ## iPhone Mirroring compatibility diff --git a/axiom-codex/skills/axiom-xcode-mcp/skills/axe-ref.md b/axiom-codex/skills/axiom-xcode-mcp/skills/axe-ref.md index b29a224f..c3657d67 100644 --- a/axiom-codex/skills/axiom-xcode-mcp/skills/axe-ref.md +++ b/axiom-codex/skills/axiom-xcode-mcp/skills/axe-ref.md @@ -25,15 +25,17 @@ axe --version axe describe-ui --udid $UDID # 2. THEN: Tap by accessibility ID (preferred) -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # 3. OR: Tap by label -axe tap --label "Login" --udid $UDID +axe tap --label "Login" --tap-style physical --udid $UDID # 4. LAST RESORT: Tap by coordinates from describe-ui output -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID ``` +**Always pass `--tap-style physical`.** AXe's default style (`automatic`) sends FBSimulator `tapAt` to anything that isn't a switch, and on Xcode 27.1 + AXe 1.8.0 that activated no SwiftUI `Button`, `List` row, `Menu`, `Menu` row, or tab — while printing `✓ … completed successfully`. `physical` (touch down/up) activated all of them (measured 2026-09-19; matrix in `axiom-tools (skills/xcui-ref.md)`). `xcui tap` adds the flag for you. + **Priority order for targeting elements:** 1. `--id` (accessibilityIdentifier) - most stable 2. `--label` (accessibility label) - stable but may change with localization @@ -45,50 +47,54 @@ axe tap -x 200 -y 400 --udid $UDID ```bash # Coordinate-based (fragile - breaks with layout changes) -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID # Accessibility-based (stable - survives UI changes) -axe tap --id "loginButton" --udid $UDID -axe tap --label "Login" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID +axe tap --label "Login" --tap-style physical --udid $UDID ``` -**Always prefer `--id` or `--label` over coordinates.** +**Always prefer `--id` or `--label` over coordinates** — with one caveat: selectors tap the element's accessibility activation point, not its pixels. A view whose accessibility frame is skewed (a label hidden with `.fixedSize()` + `.frame(width: 0).clipped()`) gets tapped off target and the tap still prints ✓. When a selector tap changes nothing, screenshot, measure the visible center, and tap `-x/-y`. ## Getting the Simulator UDID AXe requires the simulator UDID for most commands: ```bash -# Get booted simulator UDID -UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) +# List booted simulators, then pick one by name — never `head -1` +xcrun simctl list devices booted +UDID= # List all simulators axe list-simulators ``` +With more than one simulator booted, a `head -1` pick drives whichever device sorts first, and every tap on it still prints ✓. `xcui` refuses to guess in that case; direct AXe calls need you to choose. + ## Touch & Tap Commands ### Tap by Accessibility Identifier (Recommended) ```bash # Tap element with accessibilityIdentifier -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # Tap element with accessibility label -axe tap --label "Submit" --udid $UDID +axe tap --label "Submit" --tap-style physical --udid $UDID ``` ### Tap by Coordinates ```bash # Basic tap -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID # Tap with timing controls -axe tap -x 200 -y 400 --pre-delay 0.5 --post-delay 0.3 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --pre-delay 0.5 --post-delay 0.3 --udid $UDID -# Long press: use touch with --down --up --delay (tap has no hold option) -axe touch -x 200 -y 400 --down --up --delay 1.0 --udid $UDID +# Long press: use touch with --down --up --delay (tap has no hold option); +# a 1.2 s hold opened a SwiftUI .contextMenu +axe touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID ``` ### Low-Level Touch Events @@ -287,15 +293,15 @@ axe describe-ui --point 200,400 --udid $UDID UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) # Tap email field and type -axe tap --id "emailTextField" --udid $UDID +axe tap --id "emailTextField" --tap-style physical --udid $UDID axe type "user@example.com" --udid $UDID # Tap password field and type -axe tap --id "passwordTextField" --udid $UDID +axe tap --id "passwordTextField" --tap-style physical --udid $UDID axe type "password123" --udid $UDID # Tap login button -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # Wait and screenshot sleep 2 @@ -312,7 +318,7 @@ axe describe-ui --udid $UDID > /tmp/ui-tree.json cat /tmp/ui-tree.json | jq '.[] | select(.identifier != null) | {identifier, label, type}' # 3. Use discovered identifiers in automation -axe tap --id "discoveredIdentifier" --udid $UDID +axe tap --id "discoveredIdentifier" --tap-style physical --udid $UDID ``` ### Scroll to Find Element @@ -321,7 +327,7 @@ axe tap --id "discoveredIdentifier" --udid $UDID # Scroll down until element appears (pseudo-code pattern) for i in {1..5}; do if axe describe-ui --udid $UDID | grep -q "targetElement"; then - axe tap --id "targetElement" --udid $UDID + axe tap --id "targetElement" --tap-style physical --udid $UDID break fi axe gesture scroll-down --udid $UDID @@ -333,7 +339,7 @@ done ```bash # Automation with error capture -if ! axe tap --id "submitButton" --udid $UDID; then +if ! axe tap --id "submitButton" --tap-style physical --udid $UDID; then axe screenshot --output /tmp/error-state.png --udid $UDID axe describe-ui --udid $UDID > /tmp/error-ui-tree.json echo "Failed to tap submitButton - see error-state.png" @@ -353,7 +359,7 @@ Most commands support timing options: ```bash # Example with full timing control -axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --udid $UDID +axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --tap-style physical --udid $UDID ``` ## AXe vs simctl @@ -385,13 +391,17 @@ axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --udid $UDID ### Tap Doesn't Work -1. Check element is enabled (`"enabled": true` in describe-ui) -2. Try adding `--pre-delay 0.5` for slow-loading UI -3. Verify correct UDID with `axe list-simulators` +A tap that changes nothing still prints `✓ … completed successfully`, so check in this order: + +1. **Tap style** — add `--tap-style physical`. The default sends `tapAt`, which SwiftUI controls ignored in every case measured. +2. **Wrong device** — with more than one simulator booted, confirm `--udid` names the one on screen (`xcrun simctl list devices booted`). +3. **Skewed accessibility frame** — the selector resolved, but to a point off the visible control; tap measured `-x/-y` instead. +4. Check element is enabled (`"enabled": true` in describe-ui) +5. Try adding `--pre-delay 0.5` for slow-loading UI ### Type Not Working -1. Ensure text field is focused first: `axe tap --id "textField"` +1. Ensure text field is focused first: `axe tap --id "textField" --tap-style physical` 2. Check keyboard is visible 3. For unreliable input, split long strings into multiple `axe type` calls, or use `--stdin`/`--file` diff --git a/axiom-cursor/agents/simulator-tester.md b/axiom-cursor/agents/simulator-tester.md index 8762a45d..fd49ec8d 100644 --- a/axiom-cursor/agents/simulator-tester.md +++ b/axiom-cursor/agents/simulator-tester.md @@ -52,13 +52,20 @@ xcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(. # Check booted simulators xcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.state == "Booted") | {name, udid}' -# Get specific device UDID for commands -UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) +# Get specific device UDID for commands — only when exactly ONE is booted. +# With several booted (other sessions often keep their own), choose by name from the +# list above; `head -1` silently drives whichever sorts first, and its taps still print ✓. +BOOTED=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid') +if [ "$(printf '%s\n' "$BOOTED" | grep -c .)" = 1 ]; then + UDID=$BOOTED +else + echo "Several simulators are booted — set UDID to the one you mean from the list above" +fi # Boot if needed (get UDID first, then boot) xcrun simctl boot "iPhone 16 Pro" -# Preflight AXe + booted sim with xcui doctor (AXe enables real HID tap/swipe/type/describe-ui) +# Preflight AXe + booted sim with xcui doctor (AXe enables tap/swipe/type/describe-ui) if command -v axe &> /dev/null; then echo "AXe available - UI automation enabled (tap, swipe, type, describe-ui)" AXE_AVAILABLE=true @@ -256,7 +263,8 @@ xcui doctor --install # installs cameroncooke/axe/axe via brew if missing # Discover UI elements first (get accessibility identifiers) axe describe-ui --udid $UDID -# Tap by accessibility identifier (RECOMMENDED - stable) +# Tap by accessibility identifier (RECOMMENDED - stable). xcui sends a physical +# touch; bare `axe tap` needs --tap-style physical or SwiftUI controls ignore it xcui tap --id "loginButton" --udid $UDID # Tap by label @@ -265,8 +273,8 @@ xcui tap --label "Submit" --udid $UDID # Tap at coordinates (less stable) xcui tap -x 200 -y 400 --udid $UDID -# Long press -xcui tap -x 200 -y 400 --duration 1.0 --udid $UDID +# Long press (tap has no hold option; 1.2 s opens a .contextMenu) +xcui touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID # Gesture presets xcui gesture scroll-down --udid $UDID # Scroll content down @@ -321,7 +329,7 @@ xcui a11y set --toggle reduce-transparency --value on --app com.example.App xcui a11y set --toggle dynamic-type --value accessibility-extra-large ``` -Supported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`. For taps, use `xcui tap --id ` (forwards to AXe's real HID touch). Full reference: `axiom-tools (skills/xcui-ref.md)`. +Supported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`, `voiceover`. For taps, use `xcui tap --id ` (a physical touch down/up). A tap prints ✓ whether or not anything happened, so assert the result with `xcui wait`/`xcui assert` rather than trusting the tap's output. With more than one simulator booted, pass `--udid` on every xcui command — xcui refuses to guess. Full reference: `axiom-tools (skills/xcui-ref.md)`. ### 16. Network Conditioning (low-bitrate / latency / loss) diff --git a/axiom-cursor/agents/triage-analyzer.md b/axiom-cursor/agents/triage-analyzer.md index c41d15d5..b2c8b0f6 100644 --- a/axiom-cursor/agents/triage-analyzer.md +++ b/axiom-cursor/agents/triage-analyzer.md @@ -26,7 +26,9 @@ You are an expert at corpus-level production crash and hang triage. You fetch gr ## Single-Crash Escape Hatch -If the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file the `axiom_xcsym_crash` MCP tool pipeline with dSYM discovery and symbolication. This agent is for corpus triage from Sentry / ASC only. +If the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file the `axiom_xcsym_crash` MCP tool pipeline with dSYM discovery and symbolication. + +If the corpus is the **local Xcode Organizer one** — `~/Library/Developer/Xcode/Products//Crashes/Points/*.xccrashpoint` — read `axiom-shipping (skills/testflight-triage.md)`, "The On-Disk Organizer Corpus". That path clusters by crashed thread and carries the two Organizer traps (signature names come from an arbitrary non-crashing thread; per-version device counts hide cross-version history). This agent is for corpus triage from Sentry / ASC only. ## Workflow @@ -172,4 +174,4 @@ Explicit command: `/axiom-triage` [sentry|asc] ## Scope -Fetches unresolved issues, normalizes them, runs the `axiom_xcsym_triage` MCP tool to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead. +Fetches unresolved issues, normalizes them, runs the `axiom_xcsym_triage` MCP tool to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead; for the **local Organizer corpus** of `.xccrashpoint` bundles, use `axiom-shipping (skills/testflight-triage.md)`. diff --git a/axiom-cursor/reports/inventory-sha256.json b/axiom-cursor/reports/inventory-sha256.json index c039e356..41230299 100644 --- a/axiom-cursor/reports/inventory-sha256.json +++ b/axiom-cursor/reports/inventory-sha256.json @@ -133,8 +133,8 @@ }, { "path": "agents/simulator-tester.md", - "sha256": "c140fa74f8286c1c2647e45513d20fe2deafb2991e84b3cce1c83aaa8fa9b71c", - "bytes": 21093 + "sha256": "483a9c71738c1d194f7fa012a9d52f3f9a1a6f2455612a60efdc01b0ec308462", + "bytes": 21879 }, { "path": "agents/spm-conflict-resolver.md", @@ -213,8 +213,8 @@ }, { "path": "agents/triage-analyzer.md", - "sha256": "0e51e4c59067e5b474314f21ac89d099832d3221c1f7e843fceafa480994b34d", - "bytes": 11205 + "sha256": "ce8f4b35dfce8cb68e5fedb8b4da4929b5dc892f9fa93aeb36c9851c980cea45", + "bytes": 11733 }, { "path": "agents/ux-flow-auditor.md", @@ -978,13 +978,13 @@ }, { "path": "skills/axiom-integration/skills/localization-research-ref.md", - "sha256": "5015b9224e965d04313ac382148de907cdd1f18ef214050ab8aa68ace332844e", - "bytes": 16589 + "sha256": "2ca978fcc4ea1c736dba0f5de8f29a8fbb46c6707a0d08a181ae6e5b162a7d9e", + "bytes": 17999 }, { "path": "skills/axiom-integration/skills/localization.md", - "sha256": "c60212fde0d8dfce4906a9cba9354b5933d2cedd28ed3e665f9e472beb1d5888", - "bytes": 34900 + "sha256": "adc484b65485ada0c775d33e8c780e7cd9e69d6aea5e73e3d7018103dbac5054", + "bytes": 40365 }, { "path": "skills/axiom-integration/skills/privacy-ux.md", @@ -1613,8 +1613,8 @@ }, { "path": "skills/axiom-swiftui/skills/iphone-duo.md", - "sha256": "e33bab2539cc6a9620c0f7ed3ce185bf4583549cdf0864a238297ed3651cb8fb", - "bytes": 35550 + "sha256": "3d06b19fe4fc9b545efb6440ea3ac944ee01be9d5e4309146c67c48626952d4c", + "bytes": 41762 }, { "path": "skills/axiom-swiftui/skills/layout-ref.md", @@ -1708,8 +1708,8 @@ }, { "path": "skills/axiom-tools/skills/device-control-ref.md", - "sha256": "f37e2904e862fa65ee49273f6befbc08bc538f6c2944e94f464d67a48641774f", - "bytes": 25848 + "sha256": "e4a480ed405551c7fa865a1332c0c8a9de092cdba3a3acea58766416a28bb3ac", + "bytes": 29447 }, { "path": "skills/axiom-tools/skills/getting-started.md", @@ -1733,8 +1733,8 @@ }, { "path": "skills/axiom-tools/skills/xcui-ref.md", - "sha256": "fdacecb2394054b8ba58234ea06f98fc8e8a3e2722ce2d61964f46dfb9620d7b", - "bytes": 11629 + "sha256": "145d1f3934c0ea47b94295d4fd5c33e4585abf521632e1d3e954c4a19ccdd311", + "bytes": 14511 }, { "path": "skills/axiom-uikit/SKILL.md", @@ -1783,8 +1783,8 @@ }, { "path": "skills/axiom-uikit/skills/uikit-modernization.md", - "sha256": "a74552e777e484862d59ef5774551788c353e8c8503ac1da0c73a286847a5f12", - "bytes": 26333 + "sha256": "83b6fccada55a978e76e3ca7b99946d7470b922eb7e4b6472d58d151198b3b0a", + "bytes": 26924 }, { "path": "skills/axiom-vision/SKILL.md", @@ -1858,8 +1858,8 @@ }, { "path": "skills/axiom-xcode-mcp/skills/axe-ref.md", - "sha256": "47b199c7d961758f36ec06f26a4f21b3a8c7b71d3f093432b41b64c2d5a8d6f6", - "bytes": 11562 + "sha256": "b7cb79c3382494e787ab0db0054c8566c8e5791a9357c53b5da49cd0c27e0de4", + "bytes": 13419 }, { "path": "skills/axiom-xcode-mcp/skills/xcode-mcp-ref.md", @@ -1879,7 +1879,7 @@ ], "totals": { "files": 375, - "bytes": 7589185 + "bytes": 7612515 }, "excludedMirrors": 30, "classes": { diff --git a/axiom-cursor/skills/axiom-integration/skills/localization-research-ref.md b/axiom-cursor/skills/axiom-integration/skills/localization-research-ref.md index 43325223..c352c798 100644 --- a/axiom-cursor/skills/axiom-integration/skills/localization-research-ref.md +++ b/axiom-cursor/skills/axiom-integration/skills/localization-research-ref.md @@ -38,9 +38,29 @@ Do NOT use this skill for: Users expect "Shuffle," "Up Next," "Now Playing," "Smart Playlist" to mean exactly what they mean in Apple Music. Diverging from Apple's canonical translations feels wrong in every language — even if your translation is technically correct. +### Apple's own translations, on disk (most exact) + +Every installed simulator runtime ships Apple's shipped `.strings` for its bundled apps — the same text users read in Music, Mail, and Settings. `.strings` there are binary plists, so read them with `plutil`: + +```bash +RT=$(xcrun simctl list runtimes -j | python3 -c 'import json,sys; print([r["runtimeRoot"] for r in json.load(sys.stdin)["runtimes"] if r["platform"]=="iOS"][-1])') +ls "$RT/Applications/Music.app" | grep lproj # 56 locales in the 27.0 and 27.1 runtimes +plutil -p "$RT/Applications/Music.app/fr.lproj/Localizable.strings" | grep -i '"Songs"' +``` + +Music.app's keys are the English text, so grep the key and read the value: `Songs` is `Morceaux` in French and `노래` in Korean; `Shuffle` is `Aléatoire` and `임의 재생`. Some apps ship no `en.lproj` for exactly that reason — the key is the English. + +The same runtime holds hundreds of **`AppShortcuts.strings`** files (753 in the iOS 27.0 runtime), which are Apple's own registered Siri phrases — the precedent to follow for phrase *structure*, not just vocabulary: + +```bash +find "$RT" -name AppShortcuts.strings | head +``` + +Use this first when you need the exact form of a UI string. Use the Support pages below when you need what a user *says* out loud, which is a different register — the button may be `Lire` while the Siri verb is `mets`. + ### Primary Sanity Check (Authoritative) -**Apple Support multi-locale pages** — the authoritative source. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare: +**Apple Support multi-locale pages** — authoritative for spoken and user-facing phrasing. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare: | Locale | URL pattern | |--------|-------------| diff --git a/axiom-cursor/skills/axiom-integration/skills/localization.md b/axiom-cursor/skills/axiom-integration/skills/localization.md index 76389e28..962d31a9 100644 --- a/axiom-cursor/skills/axiom-integration/skills/localization.md +++ b/axiom-cursor/skills/axiom-integration/skills/localization.md @@ -90,6 +90,57 @@ Each entry has: } ``` +### Two key styles — and what a tool must read + +An entry's key is either the English source text itself or a symbolic identifier. The first entry below has the English as its key; the second uses a symbolic key and carries the English in an explicit source unit. + +```json +"Songs" : { + + }, +"quality.high" : { + "comment" : "Playback quality: 256 kbps", + "extractionState" : "extracted_with_value", + "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "High" } } } +} +``` + +`String(localized: "Songs")` produces the first; `String(localized: "quality.high", defaultValue: "High")` produces the second, with `extracted_with_value` and an explicit source unit. So **the source string is `localizations[sourceLanguage]` if present, else the key** — a tool has to handle both, not pick one. (`sourceLanguage` is a top-level field and is not always `en`: in a catalog whose source is German the unit lands under `de`, so a tool that hardcodes `en` reads a translation as if it were the source.) A tool that reads only the source localization skips every key-as-English entry, and a search over keys alone misses every symbolic one. + +### Writing a catalog from a tool + +Xcode has one byte layout, and a generic JSON writer rewrites the whole file — measured on a real 970-key catalog, `json.dump(..., indent=2)` produced a **26,531-line diff** that buried the actual translation. The layout: + +| Rule | Value | +|---|---| +| Indent | 2 spaces | +| Key separator | `" : "` — a space *before* the colon | +| Encoding | literal UTF-8, never `\u` escapes | +| Trailing newline | none | +| Empty entry | `{`, a blank line, then `}` at the entry's own indent | +| Keys inside an entry | sorted by code point | +| Keys of the top-level `strings` map | two writers, two orders — see below | + +**The two Apple writers order the top-level keys differently.** `xcstringstool` sorts by code point. The Xcode editor's order matches Foundation's `localizedStandardCompare`, which is punctuation-aware: measured across two editor-written revisions of the same 1,086- and 1,447-key catalog, that comparison had 0 violations while plain code-point order had 139 and 212. So running `xcstringstool sync` over an editor-maintained catalog re-sorts every key — a 16,005-line diff on a file whose meaning did not change. Match whichever order the file already uses. + +**`xcstringstool` ships with Xcode** and is the headless path in and out of a catalog. It is not on `PATH`; run it through `xcrun`: + +```bash +xcrun xcstringstool extract --SwiftUI --modern-localizable-strings -o /tmp/sd $(find Sources -name '*.swift') +xcrun xcstringstool sync Localizable.xcstrings --stringsdata /tmp/sd/*.stringsdata --skip-marking-strings-stale +xcrun xcstringstool print Localizable.xcstrings # every key +``` + +(`Sources/**/*.swift` is not a substitute for that `find`: macOS ships bash 3.2, which has no `globstar`, so `**` silently matches one directory level.) + +**`sync` prunes.** It is not an additive merge: every entry missing from the `.stringsdata` is marked `extractionState: stale` if it has translations and **deleted outright if it has none** — exit 0, no warning. Measured on a two-entry catalog: the untranslated entry vanished and the translated one came back stale. `--skip-marking-strings-stale` is the guard, and with it a 1,447-key catalog synced intact (122 untranslated entries all survived). + +That makes the filename matter twice over: `sync` matches a `.stringsdata` table to the catalog **by filename**, so `Localizable.xcstrings` takes the `Localizable` table. Point it at `Strings.xcstrings` and nothing matches — so nothing is added, and without the guard flag every entry is pruned or staled. + +`sync` re-emits the canonical layout only when it has something to write; a pure reformat with no source change is left exactly as it is, so it is not a repair tool for a bad write. + +To write translations — which `sync` does not do — parse, mutate only the entries you mean to touch, and re-emit in the layout above, ordering the top-level keys the way the file already orders them. In JavaScript, `JSON.parse` also hoists integer-like keys (`"30"`) to the front, so a parse/stringify round-trip reorders the file on its own; parse into an order-preserving structure instead. + ### Translation States Xcode tracks state for each translation: @@ -796,6 +847,28 @@ String(localized: "Confirm", comment: "Button to confirm delete action") **Impact**: "Confirm" could mean "verify" or "acknowledge" - context matters for accurate translation. +### Expecting a comment to separate two meanings + +A comment gives a translator context; it does **not** split a key. One English word used for two concepts is one entry with one translation, and the comments are merged: + +```swift +String(localized: "High", comment: "Energy level of a track") +String(localized: "High", comment: "Playback quality: 256 kbps") +``` + +```json +"High" : { "comment" : "Energy level of a track\nPlayback quality: 256 kbps" } +``` + +`xcstringstool sync` says so — `notice: Key "High" used with multiple comments: …` — and then the catalog ships whichever translation the translator picked for both. Give each meaning its own key and keep the English in a default value: + +```swift +String(localized: "energy.high", defaultValue: "High", comment: "Energy level of a track") +String(localized: "quality.high", defaultValue: "High", comment: "Playback quality: 256 kbps") +``` + +This bites hardest where a short word is reused across features — a picker label and a Siri phrase, say — because the wrong sense only shows up in the other feature's UI. Before renaming any enum title or button to a short common word, search the catalog for that exact key. + --- ## Troubleshooting diff --git a/axiom-cursor/skills/axiom-swiftui/skills/iphone-duo.md b/axiom-cursor/skills/axiom-swiftui/skills/iphone-duo.md index 842c42a4..0a339957 100644 --- a/axiom-cursor/skills/axiom-swiftui/skills/iphone-duo.md +++ b/axiom-cursor/skills/axiom-swiftui/skills/iphone-duo.md @@ -1,3 +1,7 @@ +## Cursor UI Tool Availability + +`xcui` is an external tool and is not bundled with the Cursor plugin; it has no Axiom MCP wrapper. Before UI automation, check `command -v xcui`. If it is absent, AXe fallback is limited to compatible input verbs: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, and `screenshot`. Then check `command -v axe` before that fallback and handle `DEVELOPER_DIR` explicitly if AXe reports a SimulatorKit loading error. AXe cannot replace `wait`, `assert`, `a11y`, `dialog`, `voiceover`, `resize`, or `doctor`. If neither tool is available, stop UI automation, explain the external setup requirement, and continue only with non-UI simulator and log checks. If AXe exists but the requested workflow requires an xcui-only capability, stop that UI workflow and report the limitation. + # iPhone Duo iPhone Duo is Apple's first two-display iPhone: an outer display, and a larger inner display that folds along a hinge. This skill covers adapting an app to it — the device model, what changes with the SDK you build against, vertical bars, the fold, and the new layout, hinge, and scene tools. SwiftUI comes first; the UIKit equivalent sits beside it. @@ -191,6 +195,35 @@ Built against the 27.1 SDK, navigation, toolbar, and tab bar items share one ver - Keyboard accessory bars stay on the keyboard. - The inner display in portrait keeps horizontal bars. +#### Host the bar in a system-managed container + +Items join the vertical bar only from inside one of the system-managed containers above. The same `.bottomBar` items on a view with none of them — in a bare `fullScreenCover`, say — render as a horizontal capsule along the bottom, while `toolbarVerticalEdge` still reads `.trailing` (measured on the 27.1 Duo simulator, closed). Adding a `NavigationStack` to a custom full-screen layer brings two side effects: + +- **The stack paints an opaque background** over whatever is behind it. Apply `.containerBackground(.clear, for: .navigation)` to the content *inside* the stack; applied to the `NavigationStack` itself it had no effect (both measured). +- **The stack gives its content the window's safe-area insets back**, even under an ancestor's `.ignoresSafeArea()` — measured trailing 84 pt and bottom 34 pt on the closed Duo, where the content outside the stack read zero. Re-apply `.ignoresSafeArea()` inside the stack if the layout depends on reading zero. + +```swift +struct PlayerCover: View { + var body: some View { + ZStack { + ArtworkBackground() + NavigationStack { + PlayerControls() + .ignoresSafeArea() + .toolbar(.hidden, for: .navigationBar) + .containerBackground(.clear, for: .navigation) // inside the stack, not on it + .toolbar { + ToolbarItem(placement: .bottomBar) { + Button("Shuffle", systemImage: "shuffle") { } + } + } + } + } + .ignoresSafeArea() + } +} +``` + #### Order items top to bottom 1. Back (automatic in a navigation container) or a custom close. SwiftUI: `.cancellationAction`. UIKit: a leading item, with `leftItemsSupplementBackButton` left `false` (the default). @@ -224,9 +257,16 @@ inboxItem.badge = .count(7) // iOS 26: a symbol-only item that still - Vertical bars have no scroll-edge effect but gain a background under Reduce Transparency — keep custom content legible either way. - A hero or background image extends under the vertical bar with `.backgroundExtensionEffect()` (SwiftUI) or `UIBackgroundExtensionView` (UIKit). +#### What your styling survives + +- **System items ignore your styling.** In the vertical bar a system `Toggle` or `Button` item ignores `.foregroundStyle` and `.opacity`; a `Toggle` shows "on" as a lit disc. `.contextMenu` on a system `Toggle` item does nothing (measured on the 27.1 Duo simulator). +- **A custom view keeps it (measured).** Opt the item in with `.axisBehavior(.verticalPreferred)` and its own `.foregroundStyle`, `.opacity`, and `.contextMenu` all work in the bar — the route for on/off/unavailable states the system rendering can't show. +- **`Menu(primaryAction:)` shows no on/off state** in the bar (reported, not re-measured). +- **The overflow menu shows each item's title and icon**, taken from its `Label`, but **no on/off state and no context menu** (reported). If state matters once an item overflows, put it in the title ("Repeat One"). + #### Plan for overflow -The outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up. A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar. +The outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up; `.visibilityPriority(.high)` works on custom items too (measured: of ten custom items in a seven-slot bar, the last two marked `.high` stayed and the three above them overflowed). A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar. #### When to turn vertical bars off @@ -236,6 +276,12 @@ A single-page, bottom-heavy layout like a calculator, or a sheet whose only item The inferred axis is usually right — a title-only item stays horizontal, an item with an image goes vertical. Override it per item when a custom view, a wide control, or a symbol↔text toggle needs a specific axis. All of these are iOS 27.1; below that, items keep whatever axis the system infers, and the knobs don't exist. A build against the 27.0 SDK or earlier never sees a vertical bar at all. +`toolbarVerticalEdge` (UIKit: `traitCollection.verticalBarEdge`) reports the system's *preferred* edge "regardless of whether a vertical bar is currently visible", and is nil (`.unspecified`) where the system never places one — hardware without a vertical bar, or a size class or orientation that doesn't use it (UIKit header doc). What the value does and doesn't tell you: + +- It is set before any toolbar item exists, and reads the same inside and outside a navigation container (measured). +- It is nil on the open inner display in portrait, half-folded included, and reads nil briefly during the opening transition (reported). Don't animate on a transient nil. +- A non-nil edge doesn't mean *your* items are vertical — outside a system-managed container they stay horizontal (Host the bar in a system-managed container). + ```swift // SwiftUI — iOS 27.1: per-item axis override, compression order, and the edge read @available(iOS 27.1, *) @@ -257,7 +303,7 @@ struct BarControls: View { } .toolbarVerticalCompressionBehavior(.prefersToolbarItems) // tab bar compresses first .overlay(alignment: .bottom) { - // .leading / .trailing while a bar is vertical; nil when items can't go vertical + // .leading / .trailing where the system places bars vertically; nil where it never does Text(edge == .trailing ? "Bar: trailing" : edge == .leading ? "Bar: leading" : "No vertical bar") .font(.caption) } @@ -530,6 +576,11 @@ Availability (`isAvailable`) and your on/off switch (`isEnabled`) are separate: ## Tooling and Testing - **Device Hub** — Xcode 27.1's Device Hub drives an iPhone Duo simulator with open, close, rotate, and fold controls (111461 0:56); Apple's overview notes the Duo simulator in Device Hub requires Xcode 27.1. The device type creates against the **iOS 27.1 runtime**; the 27.0 runtime rejects it (`Incompatible device`). `iPhone Fold` is a different product. +- **The 27.1 runtime is Duo-only.** Creating iPhone 17, 17 Pro, 17e, or iPad Pro on it fails with the same `Incompatible device` (SimError 403), so keep the 27.0 runtime for every other device (measured with Xcode 27.1). +- **Poses are Device Hub-only.** No `simctl` or `devicectl` command opens, closes, or folds the simulator. `devicectl device motion hinge-angle` only *reads* the hinge, and `devicectl device orientation set` prints success and changes nothing on the Duo (measured closed; reported open). Check the window size after every pose change: closing from open-landscape has been reported landing in closed-portrait one time and closed-landscape another. +- **Screenshots default to the inner display**, which is black while the device is closed — for `simctl io … screenshot` and `devicectl device capture` alike. Capture the outer display by name: axiom-tools (skills/device-control-ref.md, Display masks and multi-display devices). +- **Name closed poses by the hinge**, never by "turned left/right". Closed and upright, the hinge is the left edge, the camera and status bar sit top-right, and the vertical bar runs down the trailing edge (`toolbarVerticalEdge == .trailing`, measured). With the hinge along the bottom the camera is top-left and the bar moves to the leading edge; hinge along the top puts the camera bottom-right and the bar trailing (reported). The bar follows the camera's edge. +- **Synthetic taps** — send a physical touch (`xcui tap` does; bare `axe tap` needs `--tap-style physical`), or the bar's items ignore the tap while it reports ✓: axiom-tools (skills/xcui-ref.md, Tap styles). The bar's overflow (⋯) button did not open under any tap style or a 0.2–1.0 s hold in the closed pose (measured); test overflowed actions in a pose with room for them. - **Simulator gaps** — per the Xcode 27.1 beta release notes, StandBy is unavailable in the iPhone Duo Simulator runtime, and running and debugging most app extensions is unavailable there. - **App Resizability** — Xcode's app-modernization agent skill, renamed "App Resizability", now covers SwiftUI and iPhone Duo (111461 9:15). See axiom-uikit (skills/uikit-modernization.md). diff --git a/axiom-cursor/skills/axiom-tools/skills/device-control-ref.md b/axiom-cursor/skills/axiom-tools/skills/device-control-ref.md index d51049f4..5053e443 100644 --- a/axiom-cursor/skills/axiom-tools/skills/device-control-ref.md +++ b/axiom-cursor/skills/axiom-tools/skills/device-control-ref.md @@ -118,7 +118,8 @@ CI order is unchanged at the front: simctl or xcodebuild boots the sim → devic |------------|--------------|-----| | `device info displays` | works (verified) | bounds, pointScale, nativeSize, `framebufferMaskIdentifier` (exact JSON keys) | | `device capture screenshot` / `screen-record` | works (verified) | PNG / H.264 `.mp4` capture, sim or device — see Screen capture below | -| `device orientation get` (also `set`, `rotate`) | works (`get` verified) | orientation without entering the app | +| `device orientation set` / `get` (also `rotate`) | works (verified) — **except iPhone Duo**, see Rotating a simulator | rotate a sim headlessly; simctl has no rotate verb | +| `device motion hinge-angle` | works, read-only | streams the hinge angle (0° closed, 180° open) for 60 s by default; nothing in devicectl or simctl sets it | | `device settings biometrics [--enable\|--disable]` | works (verified) | enroll / unenroll Face ID / Touch ID | | `device simulate biometrics --success\|--failure` | works (verified) | drive a match / no-match | | `device settings appearance --mode light\|dark` | works (verified) | force Dark/Light; also `--look-and-feel clear\|tinted`, text size, contrast | @@ -151,6 +152,21 @@ ERROR: The capability "Get Lock State" is not supported by this device. `info lockState` is confirmed device-only; `info files`, `copy`, and `profile *` are reported device-only on simulators. In CI, treat `CoreDeviceError 1001` as "skip on simulator", not a failure. +### Rotating a simulator + +`simctl` has no rotate verb, and keystroke automation of the GUI is no substitute — it reported success while rotating nothing. Rotate from the host with devicectl and verify by screenshot **dimensions**: + +```bash +xcrun devicectl device orientation set -d "$SIM" landscapeLeft # portrait | portraitUpsideDown | landscapeLeft | landscapeRight +xcrun simctl io "$SIM" screenshot rotated.png # width and height must swap +xcrun devicectl device orientation set -d "$SIM" portrait +``` + +Verified 2026-09-19: iPhone 17 (iOS 27.0) 1206×2622 → 2622×1206, iPad Pro 11-inch (iOS 27.0) 1668×2420 → 2420×1668, and the app received the new size both times. + +- **iPhone Duo ignores it.** `set` prints `New Device Orientation: landscapeLeft`, `get` still reads `portrait`, and nothing rotates — measured closed, and reported open on the inner display too. Duo poses change only through Device Hub's pose controls: axiom-swiftui (skills/iphone-duo.md, Tooling and Testing). +- **On iPad, only the host can rotate.** An app's own `requestGeometryUpdate` is refused on iPad under the windowing model (`UISceneErrorDomain` 101; axiom-uikit (skills/uikit-modernization.md)), so a test that needs landscape should rotate from the harness with the command above, not from inside the app. + ## Screen capture — screenshot & video `devicectl device capture` is the **unified** capture path: one `-d ` selector across @@ -170,7 +186,7 @@ xcrun devicectl device capture screen-record -d --destination clip.mp4 -- | Flag | screenshot | screen-record | Notes | |------|------------|---------------|-------| | `--destination` | `.png` only | `.mp4` only | wrong extension is a hard error, not a coercion | -| `--display-unique-id` | yes | yes | pick from `device info displays`; omit = primary display | +| `--display-unique-id` | yes | yes | the `uniqueId` from `device info displays`. Omitted, the capture is **not** always the primary display — on iPhone Duo it is the inner display, even while closed and dark | | `--codec` | — | `h264` (default), `hevc` | | | `--mask-policy` | — | `ignored` (default), `premultipliedAlpha`, `black` | bezel mask for non-rectangular displays | | `--duration ` | — | auto-stop after N seconds | omit = record until SIGINT | @@ -185,10 +201,33 @@ Reach for these only when devicectl capture doesn't fit — none reach a physica | Tool | Use | Watch out | |------|-----|-----------| -| `simctl io screenshot [--type png] ` | sim PNG; `-` writes to stdout | sim only | -| `simctl io recordVideo [--codec h264\|hevc] [--mask ignored\|alpha\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only | +| `simctl io screenshot [--type png] [--display ] [--mask ignored\|alpha\|black] ` | sim PNG; `-` writes to stdout | sim only; `--display` takes a port UUID from `simctl io enumerate` (not devicectl's `uniqueId`) | +| `simctl io recordVideo [--codec h264\|hevc] [--mask ignored\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only. `--mask alpha` is accepted but unsupported for video — it renders black | | `axe record-video --output f.mp4` / `axe stream-video` | sim video / live preview stream (mjpeg, jpeg, ffmpeg, bgra) | sim only; `record-video` stops on Ctrl+C — see `axiom-xcode-mcp (skills/axe-ref.md)` | +### Display masks and multi-display devices + +For non-rectangular displays, `simctl io … screenshot --mask` chooses what the corners hold: + +| `--mask` | Corners | Use it to see | +|---|---|---| +| `alpha` | transparent (premultiplied alpha) | what the user sees on the glass | +| `ignored` | the full framebuffer rectangle | what a device screenshot captures — anything a view leaves under a corner lands in it | +| `black` | black, no alpha channel | a flat image for tools that drop alpha | + +Compare `alpha` and `ignored` when judging content near a corner. The alpha channel is also a measurement: on iPhone Duo's outer display the fully transparent run along the top and bottom rows is 18 px at the left corners and 185 px at the right (iOS 27.1 simulator). + +**A device with two displays needs the display named.** iPhone Duo's default capture — `simctl io` and `devicectl device capture` alike — is the inner display (2007×2853 px), which is black while the device is closed. Capture the outer display (1398×2034 px) explicitly: + +```bash +xcrun simctl io "$SIM" enumerate # the outer display's port is the one 1398 wide +xcrun simctl io "$SIM" screenshot --display= outer.png +xcrun devicectl device info displays -d "$SIM" # outer is "LCD", inner "LCD-1"; shows which is active +xcrun devicectl device capture screenshot -d "$SIM" --display-unique-id --destination outer.png +``` + +The two tools use different identifiers for the same display: simctl wants the **port UUID** from `enumerate`, devicectl the **`uniqueId`** from `info displays`. Guessing names fails — simctl rejects `--display=LCD-2`, and `2`, `4`, and `external` are accepted but hang without a frame. + ## Status bar for screenshots A shipping screenshot needs a clean status bar — 9:41, full bars, full battery. On a simulator @@ -392,7 +431,9 @@ trying to kill is still running. Confirm against the process, not the exit code: ```bash killall -9 Simulator DeviceHub pgrep -l Simulator DeviceHub # must print NOTHING -``` CarPlay simulation moved with it, into DeviceKit's +``` + +CarPlay simulation moved with it, into DeviceKit's `CarPlaySimulator.devicekitplugin`; the Xcode 26 `defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES` key does not exist anywhere in the 27 toolchain. diff --git a/axiom-cursor/skills/axiom-tools/skills/xcui-ref.md b/axiom-cursor/skills/axiom-tools/skills/xcui-ref.md index ec9ee747..cfef1c4f 100644 --- a/axiom-cursor/skills/axiom-tools/skills/xcui-ref.md +++ b/axiom-cursor/skills/axiom-tools/skills/xcui-ref.md @@ -4,7 +4,7 @@ # xcui Reference (Scriptable Simulator UI & Accessibility Testing) -xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself. +xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself. ## Invocation @@ -12,11 +12,13 @@ In Cursor, `xcui` is external and is not placed on `PATH` by the plugin. Check ` If it is absent, you have two working options and should say which you took: - Put it on PATH — clone the repo and symlink `.claude-plugin/plugins/axiom/bin/xcui` (see the Pi install guide). This is the only way to get `resize sweep`, `wait`, `assert`, `a11y`, `dialog`, and `voiceover`, which have no AXe equivalent. -- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards, and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided. +- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards — add `--tap-style physical` to every `axe tap` (see Tap styles) — and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided. ## Prerequisite: run `xcui doctor` -`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready; exit 2 = AXe missing or no booted sim (see `problems`/`next_steps` in the JSON). When several sims are booted, every verb targets the lowest UDID deterministically; `doctor` adds a `note` listing them, and `--udid ` (accepted by every verb, `doctor` included) targets a specific one. +`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready to drive; exit 2 = something would stop a device command — AXe missing, no booted sim, several booted with no `--udid`, or an AXe too old for the tap style xcui sends (AXe 1.7.0 added `--tap-style`). Read `problems`/`next_steps` in the JSON; `booted` lists every booted device, so a `--udid` can be picked from it without parsing prose. + +**More than one booted sim → pass `--udid` on every command.** Without it, every device verb refuses with exit 2 and lists each booted sim's UDID, name, and runtime, so the retry is one step. It refuses rather than guessing because a guess drives the wrong device while every tap still prints ✓. With one booted sim, `--udid` is optional. `doctor` doesn't refuse — it reports the devices in `booted` and fails the gate, leaving `booted_udid` empty because nothing may be targeted in that state. ## Subcommands @@ -29,7 +31,7 @@ If it is absent, you have two working options and should say which you took: - `reduce-transparency` — `defaults write com.apple.Accessibility ReduceTransparencyEnabled`; needs relaunch (pass `--app`). - `voiceover` — `devicectl device settings voiceover --enable|--disable`; `--value` is `on`/`off`. Applies live; no relaunch. The only toggle that leaves the simctl/defaults world — simctl has no VoiceOver setter. Read the state back with `xcrun devicectl device info voiceover -d `. - `xcui a11y reset` — clear xcui-set overrides (delete the defaults keys, content_size → large, increase_contrast → disabled). -- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert. +- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button with a physical touch: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert. - `xcui dialog pregrant … [--udid ]` — grant permissions ahead of time via `simctl privacy … grant`, so the dialog never appears. Services are `simctl privacy` names (`camera`, `photos`, `location`, `microphone`, `contacts`, …). Prefer this over `accept` when you control the test setup — no alert means nothing to race. - `xcui voiceover traverse [--udid ]` — emit the **computed** VoiceOver announcement sequence: walk the a11y tree in focus order (top-to-bottom, leading-to-trailing) and render each focusable element as `label, value, trait` (plus `dimmed` when disabled). Output is a `sequence` JSON array. - `xcui voiceover assert --sequence [--udid ]` — compare the live announcement sequence to an expected one; the file may be a bare JSON string array **or** a saved `traverse` report (it round-trips). Reports every differing index (one entry per mismatched position, plus a length-mismatch note when counts differ); exit `1` on any mismatch. @@ -72,16 +74,37 @@ Three `devicectl` failures are separated, because they need different fixes: ## Input — `xcui tap`, not `axe tap` -Input verbs forward to AXe verbatim: same flags, same output, same exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling comes along automatically, so guidance can't drift out of it. +Input verbs forward to AXe: the flags you pass go through untouched, with the same output and exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling and the physical tap style come along automatically, so guidance can't drift out of them. -Forwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted. `xcui tap --help` shows AXe's own flags. +Forwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted and exactly one sim is booted. `xcui tap --help` shows AXe's own flags. ```bash -xcui tap --id loginButton --udid # real HID touch, not pointer-hover +xcui tap --id loginButton --udid # physical touch down/up xcui type "user@example.com" --udid +xcui touch -x 200 -y 400 --down --up --delay 1.2 --udid # long press — tap has no hold option axe describe-ui --udid # raw a11y tree (xcui assert/wait parse this) ``` +### Tap styles + +AXe's `tap` takes `--tap-style automatic|simulator|physical`. AXe's default, `automatic`, sends a physical touch only to switches and toggles and FBSimulator `tapAt` to everything else — and `tapAt` activated none of the targets below: + +| SwiftUI target | `automatic` | `simulator` | `physical` | +|---|---|---|---| +| `Toggle` / switch | fires (the one target `automatic` sends a physical touch to) | no effect | fires | +| `Button` | no effect | no effect | fires | +| `List` row (`NavigationLink`) | no effect | no effect | pushes | +| `Button` inside a `List` | no effect | no effect | fires | +| `Menu` | no effect | no effect | opens | +| Row of an open `Menu` | no effect | no effect | fires | +| Tab of a `TabView` (iPhone) | no effect | no effect | selects | + +The switch row is AXe's documented behavior for `automatic` ("physical touch for switches/toggles and simulator tap for other targets"); the rest was measured 2026-09-19 with Xcode 27.1 and AXe 1.8.0 on iPhone 17 (iOS 27.0) and iPhone Duo (iOS 27.1) simulators, where AXe printed `✓ … completed successfully` for every no-effect cell. So `xcui tap` adds `--tap-style physical` when you pass no style (pass one to override), and `xcui dialog` taps physically. Calling `axe tap` yourself? Add `--tap-style physical`. + +**Verify the effect, never the ✓.** A tap that changes nothing prints the same success line as one that works. After any tap that matters, confirm the result with `xcui wait --for-element`, `xcui assert`, or a screenshot. + +**Selectors tap the accessibility frame, not the pixels.** `--id` and `--label` resolve to the element's accessibility activation point. When a view's accessibility frame is skewed — a label hidden with `.fixedSize()` plus `.frame(width: 0).clipped()` is one measured case, where the tap landed about 27 pt from the visible control — the tap misses and still prints ✓. Screenshot, measure the visible center, and tap `-x/-y` in points, or fix the view so its frame matches what it draws. + > **Still calling `axe` directly?** `describe-ui`, `stream-video`, and `record-video` stay bare — the first is what xcui itself parses, and the two streaming verbs outlive any request timeout. Those are the only calls that need a `DEVELOPER_DIR=` prefix, and only when `xcui doctor` reports an `axe_developer_dir` (rare — AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided). ## Output & exit codes diff --git a/axiom-cursor/skills/axiom-uikit/skills/uikit-modernization.md b/axiom-cursor/skills/axiom-uikit/skills/uikit-modernization.md index 743e3c49..d14eff8e 100644 --- a/axiom-cursor/skills/axiom-uikit/skills/uikit-modernization.md +++ b/axiom-cursor/skills/axiom-uikit/skills/uikit-modernization.md @@ -147,6 +147,7 @@ You no longer own a fixed canvas — you express preferences the user and system windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 600) ``` - **Orientation lock** — a *preference*, not a guarantee, in resizable environments. Override `UIViewController.prefersInterfaceOrientationLocked` (returns `Bool`) and call `setNeedsUpdateOfPrefersInterfaceOrientationLocked()` when it changes; read the resolved state from `windowScene.effectiveGeometry.isInterfaceOrientationLocked` (iOS 26). +- **Requesting an orientation** — iPad refuses it under the windowing model. `windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))` calls its `errorHandler` with `UISceneErrorDomain` code 101, "The current windowing mode does not allow for programmatic changes to interface orientation", and the scene keeps its size. The error handler is the only signal, so always pass one. The same request rotated an iPhone scene (measured on iPad Pro 11-inch and iPhone 17, iOS 27.0 simulators). On iPad, lay out for the size you're given instead of forcing an orientation. - **Interactive vs settled resize** — `UIWindowSceneGeometry.isInteractivelyResizing` (iOS 26) is `true` while the user drags; throttle expensive work during the drag and settle when it clears. SwiftUI's equivalent is `.onInteractiveResizeChange(_:)` (see axiom-swiftui (skills/layout-ref.md)). ## iPhone Mirroring compatibility diff --git a/axiom-cursor/skills/axiom-xcode-mcp/skills/axe-ref.md b/axiom-cursor/skills/axiom-xcode-mcp/skills/axe-ref.md index 6e7ad557..0eaa32c7 100644 --- a/axiom-cursor/skills/axiom-xcode-mcp/skills/axe-ref.md +++ b/axiom-cursor/skills/axiom-xcode-mcp/skills/axe-ref.md @@ -29,15 +29,17 @@ axe --version axe describe-ui --udid $UDID # 2. THEN: Tap by accessibility ID (preferred) -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # 3. OR: Tap by label -axe tap --label "Login" --udid $UDID +axe tap --label "Login" --tap-style physical --udid $UDID # 4. LAST RESORT: Tap by coordinates from describe-ui output -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID ``` +**Always pass `--tap-style physical`.** AXe's default style (`automatic`) sends FBSimulator `tapAt` to anything that isn't a switch, and on Xcode 27.1 + AXe 1.8.0 that activated no SwiftUI `Button`, `List` row, `Menu`, `Menu` row, or tab — while printing `✓ … completed successfully`. `physical` (touch down/up) activated all of them (measured 2026-09-19; matrix in `axiom-tools (skills/xcui-ref.md)`). `xcui tap` adds the flag for you. + **Priority order for targeting elements:** 1. `--id` (accessibilityIdentifier) - most stable 2. `--label` (accessibility label) - stable but may change with localization @@ -49,50 +51,54 @@ axe tap -x 200 -y 400 --udid $UDID ```bash # Coordinate-based (fragile - breaks with layout changes) -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID # Accessibility-based (stable - survives UI changes) -axe tap --id "loginButton" --udid $UDID -axe tap --label "Login" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID +axe tap --label "Login" --tap-style physical --udid $UDID ``` -**Always prefer `--id` or `--label` over coordinates.** +**Always prefer `--id` or `--label` over coordinates** — with one caveat: selectors tap the element's accessibility activation point, not its pixels. A view whose accessibility frame is skewed (a label hidden with `.fixedSize()` + `.frame(width: 0).clipped()`) gets tapped off target and the tap still prints ✓. When a selector tap changes nothing, screenshot, measure the visible center, and tap `-x/-y`. ## Getting the Simulator UDID AXe requires the simulator UDID for most commands: ```bash -# Get booted simulator UDID -UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) +# List booted simulators, then pick one by name — never `head -1` +xcrun simctl list devices booted +UDID= # List all simulators axe list-simulators ``` +With more than one simulator booted, a `head -1` pick drives whichever device sorts first, and every tap on it still prints ✓. `xcui` refuses to guess in that case; direct AXe calls need you to choose. + ## Touch & Tap Commands ### Tap by Accessibility Identifier (Recommended) ```bash # Tap element with accessibilityIdentifier -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # Tap element with accessibility label -axe tap --label "Submit" --udid $UDID +axe tap --label "Submit" --tap-style physical --udid $UDID ``` ### Tap by Coordinates ```bash # Basic tap -axe tap -x 200 -y 400 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --udid $UDID # Tap with timing controls -axe tap -x 200 -y 400 --pre-delay 0.5 --post-delay 0.3 --udid $UDID +axe tap -x 200 -y 400 --tap-style physical --pre-delay 0.5 --post-delay 0.3 --udid $UDID -# Long press: use touch with --down --up --delay (tap has no hold option) -axe touch -x 200 -y 400 --down --up --delay 1.0 --udid $UDID +# Long press: use touch with --down --up --delay (tap has no hold option); +# a 1.2 s hold opened a SwiftUI .contextMenu +axe touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID ``` ### Low-Level Touch Events @@ -291,15 +297,15 @@ axe describe-ui --point 200,400 --udid $UDID UDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' | head -1) # Tap email field and type -axe tap --id "emailTextField" --udid $UDID +axe tap --id "emailTextField" --tap-style physical --udid $UDID axe type "user@example.com" --udid $UDID # Tap password field and type -axe tap --id "passwordTextField" --udid $UDID +axe tap --id "passwordTextField" --tap-style physical --udid $UDID axe type "password123" --udid $UDID # Tap login button -axe tap --id "loginButton" --udid $UDID +axe tap --id "loginButton" --tap-style physical --udid $UDID # Wait and screenshot sleep 2 @@ -316,7 +322,7 @@ axe describe-ui --udid $UDID > /tmp/ui-tree.json cat /tmp/ui-tree.json | jq '.[] | select(.identifier != null) | {identifier, label, type}' # 3. Use discovered identifiers in automation -axe tap --id "discoveredIdentifier" --udid $UDID +axe tap --id "discoveredIdentifier" --tap-style physical --udid $UDID ``` ### Scroll to Find Element @@ -325,7 +331,7 @@ axe tap --id "discoveredIdentifier" --udid $UDID # Scroll down until element appears (pseudo-code pattern) for i in {1..5}; do if axe describe-ui --udid $UDID | grep -q "targetElement"; then - axe tap --id "targetElement" --udid $UDID + axe tap --id "targetElement" --tap-style physical --udid $UDID break fi axe gesture scroll-down --udid $UDID @@ -337,7 +343,7 @@ done ```bash # Automation with error capture -if ! axe tap --id "submitButton" --udid $UDID; then +if ! axe tap --id "submitButton" --tap-style physical --udid $UDID; then axe screenshot --output /tmp/error-state.png --udid $UDID axe describe-ui --udid $UDID > /tmp/error-ui-tree.json echo "Failed to tap submitButton - see error-state.png" @@ -357,7 +363,7 @@ Most commands support timing options: ```bash # Example with full timing control -axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --udid $UDID +axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --tap-style physical --udid $UDID ``` ## AXe vs simctl @@ -389,13 +395,17 @@ axe tap --id "button" --pre-delay 0.5 --post-delay 0.3 --udid $UDID ### Tap Doesn't Work -1. Check element is enabled (`"enabled": true` in describe-ui) -2. Try adding `--pre-delay 0.5` for slow-loading UI -3. Verify correct UDID with `axe list-simulators` +A tap that changes nothing still prints `✓ … completed successfully`, so check in this order: + +1. **Tap style** — add `--tap-style physical`. The default sends `tapAt`, which SwiftUI controls ignored in every case measured. +2. **Wrong device** — with more than one simulator booted, confirm `--udid` names the one on screen (`xcrun simctl list devices booted`). +3. **Skewed accessibility frame** — the selector resolved, but to a point off the visible control; tap measured `-x/-y` instead. +4. Check element is enabled (`"enabled": true` in describe-ui) +5. Try adding `--pre-delay 0.5` for slow-loading UI ### Type Not Working -1. Ensure text field is focused first: `axe tap --id "textField"` +1. Ensure text field is focused first: `axe tap --id "textField" --tap-style physical` 2. Check keyboard is visible 3. For unreliable input, split long strings into multiple `axe type` calls, or use `--stdin`/`--file` diff --git a/axiom-mcp/dist/bundle-stats.json b/axiom-mcp/dist/bundle-stats.json index bb07f1e2..93899a2b 100644 --- a/axiom-mcp/dist/bundle-stats.json +++ b/axiom-mcp/dist/bundle-stats.json @@ -1,19 +1,19 @@ { - "totalBytes": 13783901, + "totalBytes": 13822076, "skills": { "count": 303, - "bytes": 7542485 + "bytes": 7564177 }, "commands": { "count": 17, - "bytes": 48591 + "bytes": 49119 }, "agents": { "count": 42, - "bytes": 700092 + "bytes": 701425 }, "searchIndex": { - "bytes": 5492733 + "bytes": 5507355 }, - "generatedAt": "2026-09-19T17:39:49.204Z" + "generatedAt": "2026-09-19T22:27:11.517Z" } \ No newline at end of file diff --git a/axiom-mcp/dist/bundle.json b/axiom-mcp/dist/bundle.json index 87cfc680..549c94fb 100644 --- a/axiom-mcp/dist/bundle.json +++ b/axiom-mcp/dist/bundle.json @@ -1,6 +1,6 @@ { "version": "0.2.0", - "generatedAt": "2026-09-19T17:39:49.204Z", + "generatedAt": "2026-09-19T22:27:11.517Z", "skills": { "axiom-accessibility": { "name": "axiom-accessibility", @@ -17963,7 +17963,7 @@ "axiom-integration--localization-research-ref": { "name": "axiom-integration--localization-research-ref", "description": "Pre-translation and post-translation discipline for shipping localized apps that match platform conventions. String Catalogs get the mechanics right; this reference covers the research and consistency work that determines whether translations feel *native* or feel *machine-translated*.", - "content": "# Localization Research & Consistency Reference\n\n## Overview\n\nPre-translation and post-translation discipline for shipping localized apps that match platform conventions. String Catalogs get the mechanics right; this reference covers the research and consistency work that determines whether translations feel *native* or feel *machine-translated*.\n\n**Key distinction** `localization.md` covers the Xcode/xcstrings workflow (how to localize). This skill covers terminology research, termbase discipline, VoiceOver-specific translation, pseudolocalization, and TMS selection (what to localize and whether your translations are right).\n\n---\n\n## When to Use This Skill\n\nUse this skill when:\n- Matching terminology to Apple's platform vocabulary (Music, Mail, Calendar, Photos, Settings)\n- Building a project glossary before a translation pass\n- Writing translator context for VoiceOver-only strings\n- Stress-testing layout before real translations arrive\n- Choosing a Translation Management System (Crowdin, Lokalise, Phrase) for a team-scale pass\n- Preparing for Apple Design Award Inclusivity review\n- Auditing existing translations for consistency or platform-match\n\nDo NOT use this skill for:\n- Xcode String Catalog setup (use `localization.md`)\n- Plural handling, RTL, locale-aware formatting (use `localization.md`)\n- Generating translator comments automatically (see `localization.md` Part 10 — Xcode 26 AI comments)\n\n---\n\n## Related Skills\n\n- **skills/localization.md** — String Catalog mechanics, SwiftUI/UIKit APIs, plurals, RTL, formatters\n- **axiom-accessibility** — VoiceOver labels, hints, traits (the strings this skill helps translate)\n- **axiom-design** (skills/hig.md) — HIG terminology conventions\n\n---\n\n## Part 1: Matching Apple's Platform Terminology\n\nUsers expect \"Shuffle,\" \"Up Next,\" \"Now Playing,\" \"Smart Playlist\" to mean exactly what they mean in Apple Music. Diverging from Apple's canonical translations feels wrong in every language — even if your translation is technically correct.\n\n### Primary Sanity Check (Authoritative)\n\n**Apple Support multi-locale pages** — the authoritative source. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare:\n\n| Locale | URL pattern |\n|--------|-------------|\n| English (US) | `https://support.apple.com/en-us/` |\n| French | `https://support.apple.com/fr-fr/` |\n| Japanese | `https://support.apple.com/ja-jp/` |\n| Korean | `https://support.apple.com/ko-kr/` |\n| German | `https://support.apple.com/de-de/` |\n\n**Workflow**\n1. Find the relevant Apple Support article for the system app you're mirroring (e.g., \"Shuffle or repeat songs in Music\").\n2. Load it in your source locale to confirm the article covers the term.\n3. Swap the locale segment in the URL. Apple redirects to the localized version.\n4. Extract the target term as translated by Apple's localization team.\n5. Record it in your project glossary (see Part 2).\n\nUse `WebFetch` for this — the content is text-only and renders cleanly.\n\n### Fast Lookup (Community, Supplementary)\n\n**applelocalization.com** — community-built, queryable database of localized strings extracted from Apple's `.strings`/`.xcstrings` files across iOS and macOS frameworks (including `MusicUI.framework` and `MusicKit.framework`). Faster than hopping across Apple Support pages, and covers framework strings that don't appear in help articles.\n\n**When to use**\n- Need 10–30 terms quickly for an initial glossary pass.\n- Looking for a framework-level string (button label, menu item) that doesn't appear in help articles.\n- Want to see how Apple translates a term across many locales at once.\n\n**When to fall back to Apple Support**\n- The term is ambiguous or contested (multiple candidate translations).\n- You're preparing for App Store or ADA review and need a defensible source.\n- applelocalization.com returns no hits or stale data.\n\n**Framing** Treat applelocalization.com as a *sanity-check* tool, not a source of truth. It's scraped community data, not Apple-published. The Apple Support cross-check is what makes a translation defensible.\n\n### Terms Worth Looking Up First\n\nFor media/music apps, these 15 terms drive perceived quality:\n\n```\nUp Next, Now Playing, Shuffle, Repeat, Queue, Library, Playlist, Smart Playlist,\nFavorites, Downloaded, Recently Added, Recently Played, Listen Now, For You, Radio\n```\n\nFor generic apps:\n\n```\nSettings, Preferences, Done, Cancel, OK, Edit, Delete, Share, Save, Open,\nSearch, Filter, Sort, More, Show More, Show Less, Loading, Retry\n```\n\n---\n\n## Part 2: Project Termbase (Glossary)\n\nA **termbase** (or translation glossary) is a canonical mapping of app-specific terms to their localized forms. It prevents drift when future strings get added ad-hoc by different translators or AI passes.\n\n### What Belongs in a Termbase\n\n| Category | Examples |\n|----------|----------|\n| Brand words | App name, product line, feature names |\n| Feature words | \"Smart Playlist\", \"Visualizer presets\", \"Up Next\" |\n| Platform-mirroring terms | \"Shuffle\", \"Now Playing\" (from Part 1 research) |\n| Domain-specific predicates | \"is\", \"is not\", \"contains\", \"starts with\" (filter UIs) |\n| Action verbs | \"Pin\", \"Unpin\", \"Favorite\", \"Add to Library\" |\n| State labels | \"Downloaded\", \"Pending\", \"Offline\" |\n\nA working rule of thumb for a small app: 20–30 terms. Past ~50 it becomes maintenance overhead; below ~15 you'll miss the drift-prone cases. Nothing here is measured — tune it to how fast your UI vocabulary actually changes.\n\n### Termbase Format\n\nStore as markdown in the repo so translators and reviewers can diff it:\n\n```markdown\n# App Localization Glossary\n\n| English | fr | ja | ko | Source | Notes |\n|---------|----|----|----|--------|-------|\n| Up Next | À suivre | 次はこちら | 다음 항목 | Apple Music (ios 18) | Apple Music UI terminology |\n| Shuffle | Aléatoire | シャッフル | 셔플 | Apple Music (ios 18) | |\n| Smart Playlist | Liste intelligente | スマートプレイリスト | 스마트 재생목록 | Apple Music (ios 18) | |\n| Visualizer | Visualiseur | ビジュアライザ | 비주얼라이저 | Project decision 2026-04 | Custom app term |\n```\n\n**Source column is load-bearing.** It tells the next translator whether a term is locked to Apple's canonical form (don't change) or a project decision (can revisit). Without it, every new translator re-opens settled questions.\n\n### When to Build It\n\n- **Before** the first translation pass — avoids re-translation work.\n- **Before** adding a new locale — prevents drift between locales.\n- **Before** an ADA/App Store submission — makes translation choices defensible.\n- **After** a user complaint about terminology — add the corrected term with source.\n\n### Where to Store It\n\n| Location | Rationale |\n|----------|-----------|\n| `.ai/context/localization-glossary.md` | If you want AI assistants to pick it up automatically |\n| `docs/localization/glossary.md` | If contributors need it |\n| `Localization/glossary.md` next to `.xcstrings` | If translators work directly in the repo |\n\nAny of these works. What matters is that it's in version control and discoverable.\n\n---\n\n## Part 3: VoiceOver-Aware Translator Comments\n\nVoiceOver labels and hints are **spoken, not read**. They need different translation than visible labels:\n\n| String type | Visible? | Translation priority |\n|-------------|----------|---------------------|\n| `Text(\"Cancel\")` | Yes | Concise, matches platform |\n| `.accessibilityLabel(\"Cancel deletion\")` | No (spoken only) | Clear, full-sentence OK |\n| `.accessibilityHint(\"Double tap to cancel deletion of the collection\")` | No (spoken only) | Full instructional sentence |\n\n**The problem** A translator seeing `\"Cancel\"` with no context will give you the platform-match short form (`Annuler`, `キャンセル`). But if the string is actually a VoiceOver hint that needs to read as a full spoken instruction, the short form is wrong.\n\n### Convention\n\nPrefix translator comments for VoiceOver-only strings with `VoiceOver:` so translators know the audience:\n\n```swift\nString(\n localized: \"Double tap to change artwork\",\n comment: \"VoiceOver: spoken hint for the artwork button. Not visible on screen.\"\n)\n```\n\nIn a String Catalog, the comment field carries into the translator's view. Adding the `VoiceOver:` prefix lets translators (and TMS filters) identify these strings and translate them with spoken-instruction phrasing rather than UI-label phrasing.\n\n### Complementary Discipline\n\n- Pair every `.accessibilityLabel` or `.accessibilityHint` with an explicit `comment:` parameter.\n- Don't rely on Xcode 26 AI-generated comments for VoiceOver strings — the AI can't know the string isn't visible on screen. Write these comments by hand.\n- In pseudolocalization runs (Part 4), VoiceOver strings won't visually stress layout but *will* reveal truncation in screen reader output if tested with VoiceOver + Accented Pseudolanguage.\n\n---\n\n## Part 4: Pseudolocalization (Layout Stress Testing)\n\nPseudolocalization replaces source strings with lengthened, accented, or RTL-reversed variants to stress-test layout *before* real translations arrive. German runs about 20% longer than English in Apple's own UI strings — measured 1.22× the English character count across 574 framework string tables in the iOS 27.2 simulator runtime, with individual tables from 0.87× to 1.68× — and Arabic/Hebrew require full RTL layout. Finding layout breaks in pseudolocalization is cheap; finding them after paying for translations is expensive.\n\n### Xcode Scheme Options\n\nEdit scheme → Run → Options → **Application Language**:\n\n| Setting | Purpose |\n|---------|---------|\n| **Accented Pseudolanguage** | Accents every character. Measured: the character count is unchanged (16 → 16; 42 UTF-8 bytes) because the diacritics are combining marks — a character-set test, not a length test. |\n| **Right-to-Left Pseudolanguage** | Mirrors layout, reverses text direction. Stress-tests RTL without needing Arabic/Hebrew content. |\n| **Double-Length Pseudolanguage** | Duplicates every string. Extreme stress test for wrapping/truncation. |\n| **Bounded String Pseudolanguage** | Wraps each string in `[# ... #]` brackets (measured: `Welcome to WWDC!` → `[# Welcome to WWDC! #]`). Reveals non-localized (hardcoded) strings instantly. |\n\nThese options are launch arguments under the hood, which is why they can be reproduced in a scheme's Arguments pane: Bounded String sets `-NSSurroundLocalizedStrings YES`, Accented sets `-NSAccentuateLocalizedStrings YES`, Double-Length sets `-NSDoubleLocalizedStrings YES` (Xcode's own table in `IDEFoundation`). Setting `-AppleLanguages` alone selects a *language*, not one of these transforms.\n\n### When to Run Each\n\n| Goal | Mode |\n|------|------|\n| Hunting hardcoded (non-localized) strings | Bounded String |\n| Stress-testing layout before first translation pass | Accented, then Double-Length |\n| Verifying RTL layout before adding Arabic/Hebrew | Right-to-Left |\n| Final pre-submission layout check | Double-Length |\n\n### Workflow\n\n1. Set scheme to **Bounded String Pseudolanguage** first. Run through every screen. Any string that doesn't appear wrapped in `[# ... #]` is hardcoded — fix before translation.\n2. Switch to **Accented Pseudolanguage**. Run through every screen. Look for: truncated labels, buttons that wrap unexpectedly, tab bar items that overflow, toolbar items that collapse.\n3. If shipping RTL, switch to **Right-to-Left Pseudolanguage**. Verify: chevrons flip, leading/trailing alignment behaves, custom layouts mirror correctly.\n4. Run **Double-Length Pseudolanguage** as a final stress test. Anything that survives this will survive real-world German/Finnish.\n\n### Pressure Scenario\n\n**\"We don't have translators yet — can we ship without pseudolocalization?\"**\n\nNo. Pseudolocalization doesn't need translators — it's a build-time toggle that uses a synthetic language. Skipping it means layout bugs get discovered *after* paying for translations, when fixing them may require re-translating resized strings. Walking the scheme options costs one pass through your screens; discovering the same breaks post-translation costs re-translation.\n\n---\n\n## Part 5: Translation Management Systems\n\nFor team-scale translation passes across multiple locales, TMS tools manage translation memory, reviewer approval state, and `.xcstrings` round-tripping.\n\n### When You Need a TMS\n\nYou probably need a TMS when:\n- Translating into several locales at once.\n- Multiple translators or a review/approval workflow is involved.\n- You'll do repeated passes as strings evolve (translation memory becomes valuable).\n- Non-developer stakeholders (PM, marketing) need to see and approve translations.\n\nYou probably don't need a TMS when:\n- Single developer, 1–2 locales, infrequent updates.\n- Using AI translation + a single bilingual reviewer.\n- The glossary (Part 2) plus direct `.xcstrings` editing in Xcode is sufficient.\n\n### Tool Comparison\n\n| Tool | `.xcstrings` support | Translation memory | Notes |\n|------|---------------------|--------------------|-------|\n| **Crowdin** | Native | Yes | Strong collaboration UI, generous free tier for open source |\n| **Lokalise** | Native | Yes | Best-in-class API, good for CI integration |\n| **Phrase** | Native | Yes | Strong glossary/termbase features, VoiceOver i18n guide |\n| **SimpleLocalize** | Native | Yes | Lighter weight, simpler pricing |\n\nAll four import `.xcstrings` directly (no conversion step). Most preserve the `state` field — Xcode's serialized values are `new`, `needs_review` and `translated`; staleness is a separate `extractionState` (`stale`), not a `state`. Verify with your chosen tool's current docs before committing to a round-trip workflow.\n\n### Round-Trip Workflow\n\n```\nXcode → export .xcstrings → TMS import → translate/review → TMS export → replace .xcstrings → Xcode\n```\n\n1. Commit `.xcstrings` with current source strings.\n2. Upload to TMS (most have CLI or Xcode integration).\n3. Translators work in TMS UI (with termbase, translation memory, comments).\n4. Reviewer transitions state from `needs_review` → `translated`.\n5. Export back to `.xcstrings`.\n6. Commit the updated file. Build to verify.\n\n### Integrating With the Termbase (Part 2)\n\nAll four tools accept a termbase/glossary upload (CSV or TBX format). Import your project glossary so translators see canonical terms inline and can't accidentally diverge from Apple's platform terminology.\n\n---\n\n## End-to-End Pre-Submission Workflow\n\nFor an app preparing for ADA review or App Store submission with fresh translations:\n\n1. **Pseudolocalize first** (Part 4, Bounded String mode) — fix all hardcoded strings.\n2. **Research Apple terms** (Part 1) — the most-prominent UI terms, cross-checked against Apple Support multi-locale pages.\n3. **Build the glossary** (Part 2) — commit to `.ai/context/localization-glossary.md` or equivalent.\n4. **Audit VoiceOver comments** (Part 3) — every `.accessibilityLabel`/`.accessibilityHint` has a `VoiceOver:`-prefixed translator comment.\n5. **Enable Xcode 26 AI comment generation** (see `localization.md` Part 10) — fills in translator context for remaining strings.\n6. **Translate** — via TMS (Part 5) if team-scale, or directly in Xcode + glossary if solo.\n7. **Pseudolocalize again** (Accented + Double-Length) — verify translated layout still works.\n8. **Device test** — real devices in target locales, VoiceOver on, all screens walked through.\n\nThis sequence catches layout issues before paying for translation, catches terminology drift before users complain, and produces a defensible paper trail for ADA Inclusivity review.\n\n---\n\n## Resources\n\n**Apple**: /xcode/localizing-and-varying-text-with-a-string-catalog, /xcode/localization, /accessibility/voiceover\n\n**WWDC**: 2025-225 (Xcode 26 localization), 2023-10155 (String Catalogs), 2021-10221 (Streamline your localized strings)\n\n**Community**: applelocalization.com (community-built localized strings database — sanity-check, not authoritative)\n\n**Support pages** (authoritative cross-check): support.apple.com/en-us → swap locale segment (fr-fr, ja-jp, ko-kr, de-de, etc.)\n\n**TMS**: Crowdin, Lokalise, Phrase, SimpleLocalize (all support `.xcstrings` natively)\n\n**Skills**: skills/localization.md (String Catalog mechanics), axiom-accessibility (VoiceOver strings this skill helps translate), axiom-design (skills/hig.md, HIG terminology conventions)\n", + "content": "# Localization Research & Consistency Reference\n\n## Overview\n\nPre-translation and post-translation discipline for shipping localized apps that match platform conventions. String Catalogs get the mechanics right; this reference covers the research and consistency work that determines whether translations feel *native* or feel *machine-translated*.\n\n**Key distinction** `localization.md` covers the Xcode/xcstrings workflow (how to localize). This skill covers terminology research, termbase discipline, VoiceOver-specific translation, pseudolocalization, and TMS selection (what to localize and whether your translations are right).\n\n---\n\n## When to Use This Skill\n\nUse this skill when:\n- Matching terminology to Apple's platform vocabulary (Music, Mail, Calendar, Photos, Settings)\n- Building a project glossary before a translation pass\n- Writing translator context for VoiceOver-only strings\n- Stress-testing layout before real translations arrive\n- Choosing a Translation Management System (Crowdin, Lokalise, Phrase) for a team-scale pass\n- Preparing for Apple Design Award Inclusivity review\n- Auditing existing translations for consistency or platform-match\n\nDo NOT use this skill for:\n- Xcode String Catalog setup (use `localization.md`)\n- Plural handling, RTL, locale-aware formatting (use `localization.md`)\n- Generating translator comments automatically (see `localization.md` Part 10 — Xcode 26 AI comments)\n\n---\n\n## Related Skills\n\n- **skills/localization.md** — String Catalog mechanics, SwiftUI/UIKit APIs, plurals, RTL, formatters\n- **axiom-accessibility** — VoiceOver labels, hints, traits (the strings this skill helps translate)\n- **axiom-design** (skills/hig.md) — HIG terminology conventions\n\n---\n\n## Part 1: Matching Apple's Platform Terminology\n\nUsers expect \"Shuffle,\" \"Up Next,\" \"Now Playing,\" \"Smart Playlist\" to mean exactly what they mean in Apple Music. Diverging from Apple's canonical translations feels wrong in every language — even if your translation is technically correct.\n\n### Apple's own translations, on disk (most exact)\n\nEvery installed simulator runtime ships Apple's shipped `.strings` for its bundled apps — the same text users read in Music, Mail, and Settings. `.strings` there are binary plists, so read them with `plutil`:\n\n```bash\nRT=$(xcrun simctl list runtimes -j | python3 -c 'import json,sys; print([r[\"runtimeRoot\"] for r in json.load(sys.stdin)[\"runtimes\"] if r[\"platform\"]==\"iOS\"][-1])')\nls \"$RT/Applications/Music.app\" | grep lproj # 56 locales in the 27.0 and 27.1 runtimes\nplutil -p \"$RT/Applications/Music.app/fr.lproj/Localizable.strings\" | grep -i '\"Songs\"'\n```\n\nMusic.app's keys are the English text, so grep the key and read the value: `Songs` is `Morceaux` in French and `노래` in Korean; `Shuffle` is `Aléatoire` and `임의 재생`. Some apps ship no `en.lproj` for exactly that reason — the key is the English.\n\nThe same runtime holds hundreds of **`AppShortcuts.strings`** files (753 in the iOS 27.0 runtime), which are Apple's own registered Siri phrases — the precedent to follow for phrase *structure*, not just vocabulary:\n\n```bash\nfind \"$RT\" -name AppShortcuts.strings | head\n```\n\nUse this first when you need the exact form of a UI string. Use the Support pages below when you need what a user *says* out loud, which is a different register — the button may be `Lire` while the Siri verb is `mets`.\n\n### Primary Sanity Check (Authoritative)\n\n**Apple Support multi-locale pages** — authoritative for spoken and user-facing phrasing. Apple's help articles ship in every supported locale with hand-translated terminology. Fetch the same article across locales and compare:\n\n| Locale | URL pattern |\n|--------|-------------|\n| English (US) | `https://support.apple.com/en-us/` |\n| French | `https://support.apple.com/fr-fr/` |\n| Japanese | `https://support.apple.com/ja-jp/` |\n| Korean | `https://support.apple.com/ko-kr/` |\n| German | `https://support.apple.com/de-de/` |\n\n**Workflow**\n1. Find the relevant Apple Support article for the system app you're mirroring (e.g., \"Shuffle or repeat songs in Music\").\n2. Load it in your source locale to confirm the article covers the term.\n3. Swap the locale segment in the URL. Apple redirects to the localized version.\n4. Extract the target term as translated by Apple's localization team.\n5. Record it in your project glossary (see Part 2).\n\nUse `WebFetch` for this — the content is text-only and renders cleanly.\n\n### Fast Lookup (Community, Supplementary)\n\n**applelocalization.com** — community-built, queryable database of localized strings extracted from Apple's `.strings`/`.xcstrings` files across iOS and macOS frameworks (including `MusicUI.framework` and `MusicKit.framework`). Faster than hopping across Apple Support pages, and covers framework strings that don't appear in help articles.\n\n**When to use**\n- Need 10–30 terms quickly for an initial glossary pass.\n- Looking for a framework-level string (button label, menu item) that doesn't appear in help articles.\n- Want to see how Apple translates a term across many locales at once.\n\n**When to fall back to Apple Support**\n- The term is ambiguous or contested (multiple candidate translations).\n- You're preparing for App Store or ADA review and need a defensible source.\n- applelocalization.com returns no hits or stale data.\n\n**Framing** Treat applelocalization.com as a *sanity-check* tool, not a source of truth. It's scraped community data, not Apple-published. The Apple Support cross-check is what makes a translation defensible.\n\n### Terms Worth Looking Up First\n\nFor media/music apps, these 15 terms drive perceived quality:\n\n```\nUp Next, Now Playing, Shuffle, Repeat, Queue, Library, Playlist, Smart Playlist,\nFavorites, Downloaded, Recently Added, Recently Played, Listen Now, For You, Radio\n```\n\nFor generic apps:\n\n```\nSettings, Preferences, Done, Cancel, OK, Edit, Delete, Share, Save, Open,\nSearch, Filter, Sort, More, Show More, Show Less, Loading, Retry\n```\n\n---\n\n## Part 2: Project Termbase (Glossary)\n\nA **termbase** (or translation glossary) is a canonical mapping of app-specific terms to their localized forms. It prevents drift when future strings get added ad-hoc by different translators or AI passes.\n\n### What Belongs in a Termbase\n\n| Category | Examples |\n|----------|----------|\n| Brand words | App name, product line, feature names |\n| Feature words | \"Smart Playlist\", \"Visualizer presets\", \"Up Next\" |\n| Platform-mirroring terms | \"Shuffle\", \"Now Playing\" (from Part 1 research) |\n| Domain-specific predicates | \"is\", \"is not\", \"contains\", \"starts with\" (filter UIs) |\n| Action verbs | \"Pin\", \"Unpin\", \"Favorite\", \"Add to Library\" |\n| State labels | \"Downloaded\", \"Pending\", \"Offline\" |\n\nA working rule of thumb for a small app: 20–30 terms. Past ~50 it becomes maintenance overhead; below ~15 you'll miss the drift-prone cases. Nothing here is measured — tune it to how fast your UI vocabulary actually changes.\n\n### Termbase Format\n\nStore as markdown in the repo so translators and reviewers can diff it:\n\n```markdown\n# App Localization Glossary\n\n| English | fr | ja | ko | Source | Notes |\n|---------|----|----|----|--------|-------|\n| Up Next | À suivre | 次はこちら | 다음 항목 | Apple Music (ios 18) | Apple Music UI terminology |\n| Shuffle | Aléatoire | シャッフル | 셔플 | Apple Music (ios 18) | |\n| Smart Playlist | Liste intelligente | スマートプレイリスト | 스마트 재생목록 | Apple Music (ios 18) | |\n| Visualizer | Visualiseur | ビジュアライザ | 비주얼라이저 | Project decision 2026-04 | Custom app term |\n```\n\n**Source column is load-bearing.** It tells the next translator whether a term is locked to Apple's canonical form (don't change) or a project decision (can revisit). Without it, every new translator re-opens settled questions.\n\n### When to Build It\n\n- **Before** the first translation pass — avoids re-translation work.\n- **Before** adding a new locale — prevents drift between locales.\n- **Before** an ADA/App Store submission — makes translation choices defensible.\n- **After** a user complaint about terminology — add the corrected term with source.\n\n### Where to Store It\n\n| Location | Rationale |\n|----------|-----------|\n| `.ai/context/localization-glossary.md` | If you want AI assistants to pick it up automatically |\n| `docs/localization/glossary.md` | If contributors need it |\n| `Localization/glossary.md` next to `.xcstrings` | If translators work directly in the repo |\n\nAny of these works. What matters is that it's in version control and discoverable.\n\n---\n\n## Part 3: VoiceOver-Aware Translator Comments\n\nVoiceOver labels and hints are **spoken, not read**. They need different translation than visible labels:\n\n| String type | Visible? | Translation priority |\n|-------------|----------|---------------------|\n| `Text(\"Cancel\")` | Yes | Concise, matches platform |\n| `.accessibilityLabel(\"Cancel deletion\")` | No (spoken only) | Clear, full-sentence OK |\n| `.accessibilityHint(\"Double tap to cancel deletion of the collection\")` | No (spoken only) | Full instructional sentence |\n\n**The problem** A translator seeing `\"Cancel\"` with no context will give you the platform-match short form (`Annuler`, `キャンセル`). But if the string is actually a VoiceOver hint that needs to read as a full spoken instruction, the short form is wrong.\n\n### Convention\n\nPrefix translator comments for VoiceOver-only strings with `VoiceOver:` so translators know the audience:\n\n```swift\nString(\n localized: \"Double tap to change artwork\",\n comment: \"VoiceOver: spoken hint for the artwork button. Not visible on screen.\"\n)\n```\n\nIn a String Catalog, the comment field carries into the translator's view. Adding the `VoiceOver:` prefix lets translators (and TMS filters) identify these strings and translate them with spoken-instruction phrasing rather than UI-label phrasing.\n\n### Complementary Discipline\n\n- Pair every `.accessibilityLabel` or `.accessibilityHint` with an explicit `comment:` parameter.\n- Don't rely on Xcode 26 AI-generated comments for VoiceOver strings — the AI can't know the string isn't visible on screen. Write these comments by hand.\n- In pseudolocalization runs (Part 4), VoiceOver strings won't visually stress layout but *will* reveal truncation in screen reader output if tested with VoiceOver + Accented Pseudolanguage.\n\n---\n\n## Part 4: Pseudolocalization (Layout Stress Testing)\n\nPseudolocalization replaces source strings with lengthened, accented, or RTL-reversed variants to stress-test layout *before* real translations arrive. German runs about 20% longer than English in Apple's own UI strings — measured 1.22× the English character count across 574 framework string tables in the iOS 27.2 simulator runtime, with individual tables from 0.87× to 1.68× — and Arabic/Hebrew require full RTL layout. Finding layout breaks in pseudolocalization is cheap; finding them after paying for translations is expensive.\n\n### Xcode Scheme Options\n\nEdit scheme → Run → Options → **Application Language**:\n\n| Setting | Purpose |\n|---------|---------|\n| **Accented Pseudolanguage** | Accents every character. Measured: the character count is unchanged (16 → 16; 42 UTF-8 bytes) because the diacritics are combining marks — a character-set test, not a length test. |\n| **Right-to-Left Pseudolanguage** | Mirrors layout, reverses text direction. Stress-tests RTL without needing Arabic/Hebrew content. |\n| **Double-Length Pseudolanguage** | Duplicates every string. Extreme stress test for wrapping/truncation. |\n| **Bounded String Pseudolanguage** | Wraps each string in `[# ... #]` brackets (measured: `Welcome to WWDC!` → `[# Welcome to WWDC! #]`). Reveals non-localized (hardcoded) strings instantly. |\n\nThese options are launch arguments under the hood, which is why they can be reproduced in a scheme's Arguments pane: Bounded String sets `-NSSurroundLocalizedStrings YES`, Accented sets `-NSAccentuateLocalizedStrings YES`, Double-Length sets `-NSDoubleLocalizedStrings YES` (Xcode's own table in `IDEFoundation`). Setting `-AppleLanguages` alone selects a *language*, not one of these transforms.\n\n### When to Run Each\n\n| Goal | Mode |\n|------|------|\n| Hunting hardcoded (non-localized) strings | Bounded String |\n| Stress-testing layout before first translation pass | Accented, then Double-Length |\n| Verifying RTL layout before adding Arabic/Hebrew | Right-to-Left |\n| Final pre-submission layout check | Double-Length |\n\n### Workflow\n\n1. Set scheme to **Bounded String Pseudolanguage** first. Run through every screen. Any string that doesn't appear wrapped in `[# ... #]` is hardcoded — fix before translation.\n2. Switch to **Accented Pseudolanguage**. Run through every screen. Look for: truncated labels, buttons that wrap unexpectedly, tab bar items that overflow, toolbar items that collapse.\n3. If shipping RTL, switch to **Right-to-Left Pseudolanguage**. Verify: chevrons flip, leading/trailing alignment behaves, custom layouts mirror correctly.\n4. Run **Double-Length Pseudolanguage** as a final stress test. Anything that survives this will survive real-world German/Finnish.\n\n### Pressure Scenario\n\n**\"We don't have translators yet — can we ship without pseudolocalization?\"**\n\nNo. Pseudolocalization doesn't need translators — it's a build-time toggle that uses a synthetic language. Skipping it means layout bugs get discovered *after* paying for translations, when fixing them may require re-translating resized strings. Walking the scheme options costs one pass through your screens; discovering the same breaks post-translation costs re-translation.\n\n---\n\n## Part 5: Translation Management Systems\n\nFor team-scale translation passes across multiple locales, TMS tools manage translation memory, reviewer approval state, and `.xcstrings` round-tripping.\n\n### When You Need a TMS\n\nYou probably need a TMS when:\n- Translating into several locales at once.\n- Multiple translators or a review/approval workflow is involved.\n- You'll do repeated passes as strings evolve (translation memory becomes valuable).\n- Non-developer stakeholders (PM, marketing) need to see and approve translations.\n\nYou probably don't need a TMS when:\n- Single developer, 1–2 locales, infrequent updates.\n- Using AI translation + a single bilingual reviewer.\n- The glossary (Part 2) plus direct `.xcstrings` editing in Xcode is sufficient.\n\n### Tool Comparison\n\n| Tool | `.xcstrings` support | Translation memory | Notes |\n|------|---------------------|--------------------|-------|\n| **Crowdin** | Native | Yes | Strong collaboration UI, generous free tier for open source |\n| **Lokalise** | Native | Yes | Best-in-class API, good for CI integration |\n| **Phrase** | Native | Yes | Strong glossary/termbase features, VoiceOver i18n guide |\n| **SimpleLocalize** | Native | Yes | Lighter weight, simpler pricing |\n\nAll four import `.xcstrings` directly (no conversion step). Most preserve the `state` field — Xcode's serialized values are `new`, `needs_review` and `translated`; staleness is a separate `extractionState` (`stale`), not a `state`. Verify with your chosen tool's current docs before committing to a round-trip workflow.\n\n### Round-Trip Workflow\n\n```\nXcode → export .xcstrings → TMS import → translate/review → TMS export → replace .xcstrings → Xcode\n```\n\n1. Commit `.xcstrings` with current source strings.\n2. Upload to TMS (most have CLI or Xcode integration).\n3. Translators work in TMS UI (with termbase, translation memory, comments).\n4. Reviewer transitions state from `needs_review` → `translated`.\n5. Export back to `.xcstrings`.\n6. Commit the updated file. Build to verify.\n\n### Integrating With the Termbase (Part 2)\n\nAll four tools accept a termbase/glossary upload (CSV or TBX format). Import your project glossary so translators see canonical terms inline and can't accidentally diverge from Apple's platform terminology.\n\n---\n\n## End-to-End Pre-Submission Workflow\n\nFor an app preparing for ADA review or App Store submission with fresh translations:\n\n1. **Pseudolocalize first** (Part 4, Bounded String mode) — fix all hardcoded strings.\n2. **Research Apple terms** (Part 1) — the most-prominent UI terms, cross-checked against Apple Support multi-locale pages.\n3. **Build the glossary** (Part 2) — commit to `.ai/context/localization-glossary.md` or equivalent.\n4. **Audit VoiceOver comments** (Part 3) — every `.accessibilityLabel`/`.accessibilityHint` has a `VoiceOver:`-prefixed translator comment.\n5. **Enable Xcode 26 AI comment generation** (see `localization.md` Part 10) — fills in translator context for remaining strings.\n6. **Translate** — via TMS (Part 5) if team-scale, or directly in Xcode + glossary if solo.\n7. **Pseudolocalize again** (Accented + Double-Length) — verify translated layout still works.\n8. **Device test** — real devices in target locales, VoiceOver on, all screens walked through.\n\nThis sequence catches layout issues before paying for translation, catches terminology drift before users complain, and produces a defensible paper trail for ADA Inclusivity review.\n\n---\n\n## Resources\n\n**Apple**: /xcode/localizing-and-varying-text-with-a-string-catalog, /xcode/localization, /accessibility/voiceover\n\n**WWDC**: 2025-225 (Xcode 26 localization), 2023-10155 (String Catalogs), 2021-10221 (Streamline your localized strings)\n\n**Community**: applelocalization.com (community-built localized strings database — sanity-check, not authoritative)\n\n**Support pages** (authoritative cross-check): support.apple.com/en-us → swap locale segment (fr-fr, ja-jp, ko-kr, de-de, etc.)\n\n**TMS**: Crowdin, Lokalise, Phrase, SimpleLocalize (all support `.xcstrings` natively)\n\n**Skills**: skills/localization.md (String Catalog mechanics), axiom-accessibility (VoiceOver strings this skill helps translate), axiom-design (skills/hig.md, HIG terminology conventions)\n", "skillType": "reference", "source": "axiom", "tags": [ @@ -18006,56 +18006,56 @@ "heading": "Part 1: Matching Apple's Platform Terminology", "level": 2, "startLine": 36, - "endLine": 94, - "charCount": 2913 + "endLine": 114, + "charCount": 4302 }, { "heading": "Part 2: Project Termbase (Glossary)", "level": 2, - "startLine": 95, - "endLine": 116, + "startLine": 115, + "endLine": 136, "charCount": 1070 }, { "heading": "App Localization Glossary", "level": 1, - "startLine": 117, - "endLine": 147, + "startLine": 137, + "endLine": 167, "charCount": 1430 }, { "heading": "Part 3: VoiceOver-Aware Translator Comments", "level": 2, - "startLine": 148, - "endLine": 180, + "startLine": 168, + "endLine": 200, "charCount": 1782 }, { "heading": "Part 4: Pseudolocalization (Layout Stress Testing)", "level": 2, - "startLine": 181, - "endLine": 221, + "startLine": 201, + "endLine": 241, "charCount": 3247 }, { "heading": "Part 5: Translation Management Systems", "level": 2, - "startLine": 222, - "endLine": 268, + "startLine": 242, + "endLine": 288, "charCount": 2269 }, { "heading": "End-to-End Pre-Submission Workflow", "level": 2, - "startLine": 269, - "endLine": 285, + "startLine": 289, + "endLine": 305, "charCount": 1177 }, { "heading": "Resources", "level": 2, - "startLine": 286, - "endLine": 299, + "startLine": 306, + "endLine": 319, "charCount": 775 } ], @@ -18074,7 +18074,7 @@ "axiom-integration--localization": { "name": "axiom-integration--localization", "description": "Comprehensive guide to app localization using String Catalogs. Apple Design Award Inclusivity winners always support multiple languages with excellent RTL (Right-to-Left) support.", - "content": "\n# Localization & Internationalization\n\nComprehensive guide to app localization using String Catalogs. Apple Design Award Inclusivity winners always support multiple languages with excellent RTL (Right-to-Left) support.\n\n## Overview\n\nString Catalogs (`.xcstrings`) are Xcode 15's unified format for managing app localization. They replace legacy `.strings` and `.stringsdict` files with a single JSON-based format that's easier to maintain, diff, and integrate with translation workflows.\n\nThis skill covers String Catalogs, SwiftUI/UIKit localization APIs, plural handling, RTL support, locale-aware formatting, and migration strategies from legacy formats.\n\n## When to Use This Skill\n\n- Setting up String Catalogs in Xcode 15+\n- Localizing SwiftUI and UIKit apps\n- Handling plural forms correctly (critical for many languages)\n- Supporting RTL languages (Arabic, Hebrew)\n- Formatting dates, numbers, and currencies by locale\n- Migrating from legacy `.strings`/`.stringsdict` files\n- Preparing App Shortcuts and App Intents for localization\n- Debugging missing translations or incorrect plural forms\n\n## System Requirements\n\n- **Xcode 15+** for String Catalogs (`.xcstrings`)\n- **Xcode 26+** for automatic symbol generation, `#bundle` macro, and AI-powered comment generation\n- **iOS 16+** for `LocalizedStringResource` (`@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)`)\n- **iOS 16+** for App Shortcuts localization\n- Earlier iOS versions use legacy `.strings` files\n\n---\n\n## Part 1: String Catalogs (WWDC 2023/10155)\n\n### Creating a String Catalog\n\n**Method 1: Xcode Navigator**\n1. File → New → File\n2. Choose \"String Catalog\"\n3. Name it (e.g., `Localizable.xcstrings`)\n4. Add to target\n\n**Method 2: Automatic Extraction**\n\nXcode 15 can automatically extract strings from:\n- SwiftUI views (string literals in `Text`, `Label`, `Button`)\n- Swift code (`String(localized:)`)\n- Objective-C (`NSLocalizedString`)\n- C (`CFCopyLocalizedString`)\n- Interface Builder files (`.storyboard`, `.xib`)\n- Info.plist values\n- App Shortcuts phrases\n\n**Build Settings Required**:\n- **\"Use Compiler to Extract Swift Strings\"** → Yes\n- **\"Localization Prefers String Catalogs\"** → Yes\n\n### String Catalog Structure\n\nEach entry has:\n- **Key**: Unique identifier (default: the English string)\n- **Default Value**: Fallback if translation missing\n- **Comment**: Context for translators\n- **String Table**: Organization container (default: \"Localizable\")\n\n**Example `.xcstrings` JSON**:\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Thanks for shopping with us!\" : {\n \"comment\" : \"Label above checkout button\",\n \"localizations\" : {\n \"en\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Thanks for shopping with us!\"\n }\n },\n \"es\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"¡Gracias por comprar con nosotros!\"\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n### Translation States\n\nXcode tracks state for each translation:\n\n- **New** (⚪) - String hasn't been translated yet\n- **Needs Review** (🟡) - Source changed, translation may be outdated\n- **Reviewed** (✅) - Translation approved and current\n- **Stale** (🔴) - String no longer found in source code\n\nIn the file itself the `state` values are `new`, `needs_review` and `translated` — \"Reviewed\" is the UI's word for `translated`. Staleness is not a `state`: Xcode writes it to `extractionState` as `stale`.\n\n**Workflow**:\n1. Developer adds string → **New**\n2. Translator adds translation → **Reviewed**\n3. Developer changes source → **Needs Review**\n4. Translator updates → **Reviewed**\n5. Developer removes code → **Stale**\n\n---\n\n## Part 2: SwiftUI Localization\n\n### LocalizedStringKey (Automatic)\n\nSwiftUI views with `String` parameters automatically support localization:\n\n```swift\n// ✅ Automatically localizable\nText(\"Welcome to WWDC!\")\nLabel(\"Thanks for shopping with us!\", systemImage: \"bag\")\nButton(\"Checkout\") { }\n\n// Xcode extracts these strings to String Catalog\n```\n\n**How it works**: SwiftUI uses `LocalizedStringKey` internally, which looks up strings in String Catalogs.\n\n### String(localized:) with Comments\n\nFor explicit localization in Swift code:\n\n```swift\n// Basic\nlet title = String(localized: \"Welcome to WWDC!\")\n\n// With comment for translators\nlet title = String(localized: \"Welcome to WWDC!\",\n comment: \"Notification banner title\")\n\n// With custom table\nlet title = String(localized: \"Welcome to WWDC!\",\n table: \"WWDCNotifications\",\n comment: \"Notification banner title\")\n\n// With default value (key ≠ English text)\nlet title = String(localized: \"WWDC_NOTIFICATION_TITLE\",\n defaultValue: \"Welcome to WWDC!\",\n comment: \"Notification banner title\")\n```\n\n**Best practice**: Always include `comment` to give translators context.\n\n### LocalizedStringResource (Deferred Localization)\n\nFor passing localizable strings to other functions:\n\n```swift\nimport Foundation\n\nstruct CardView: View {\n let title: LocalizedStringResource\n let subtitle: LocalizedStringResource\n\n var body: some View {\n ZStack {\n RoundedRectangle(cornerRadius: 10.0)\n VStack {\n Text(title) // Resolved at render time\n Text(subtitle)\n }\n .padding()\n }\n }\n}\n\n// Usage\nCardView(\n title: \"Recent Purchases\",\n subtitle: \"Items you've ordered in the past week.\"\n)\n```\n\n**Key difference**: `LocalizedStringResource` defers lookup until used, allowing custom views to be fully localizable.\n\n### AttributedString with Markdown\n\n```swift\n// Markdown formatting is preserved across localizations\nlet subtitle = AttributedString(localized: \"**Bold** and _italic_ text\")\n```\n\n---\n\n## Part 3: UIKit & Foundation\n\n### NSLocalizedString Macro\n\n```swift\n// Basic\nlet title = NSLocalizedString(\"Recent Purchases\", comment: \"Button Title\")\n\n// With table\nlet title = NSLocalizedString(\"Recent Purchases\",\n tableName: \"Shopping\",\n comment: \"Button Title\")\n\n// With bundle\nlet title = NSLocalizedString(\"Recent Purchases\",\n tableName: nil,\n bundle: .main,\n value: \"\",\n comment: \"Button Title\")\n```\n\n### Bundle.localizedString\n\n```swift\nlet customBundle = Bundle(for: MyFramework.self)\nlet text = customBundle.localizedString(forKey: \"Welcome\",\n value: nil,\n table: \"MyFramework\")\n```\n\n### Custom Macros\n\n```objc\n// Objective-C\n#define MyLocalizedString(key, comment) \\\n [myBundle localizedStringForKey:key value:nil table:nil]\n```\n\n### Info.plist Localization\n\nLocalize app name, permissions, etc.:\n\n1. Select `Info.plist`\n2. Editor → Add Localization\n3. Create `InfoPlist.strings` for each language:\n\n```\n// InfoPlist.strings (Spanish)\n\"CFBundleName\" = \"Mi Aplicación\";\n\"NSCameraUsageDescription\" = \"La app necesita acceso a la cámara para tomar fotos.\";\n```\n\n---\n\n## Part 4: Pluralization\n\nDifferent languages have different plural rules:\n\n- **English**: 2 forms (one, other)\n- **Russian**: 4 forms (one, few, many, other)\n- **Polish**: 4 forms (one, few, many, other)\n- **Arabic**: 6 forms (zero, one, two, few, many, other)\n\nXcode's own table (`XCStringsParser.framework/Resources/variationConfigFile.plist`, 711 languages) is the authority for these sets — `ru` and `pl` both list `one, few, many, other`. What matters for a translation is the number of variations the catalog can hold, not the number of forms a language is usually described with.\n\n### SwiftUI Plural Handling\n\n```swift\n// The extractor writes a typed key; the plural variation is added in the catalog\nText(\"\\(count) items\") // key: \"%lld items\"\nText(\"\\(visitorCount) Recent Visitors\") // key: \"%lld Recent Visitors\"\n```\n\n**In String Catalog** (the two top-level keys `sourceLanguage` and `version` are required):\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"%lld Recent Visitors\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"plural\" : {\n \"one\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"%lld Recent Visitor\"\n }\n },\n \"other\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"%lld Recent Visitors\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n**Xcode does not create those variations for you.** Extraction records the key and stops: a `%lld` entry with no `variations` stays a flat `stringUnit`, and a string newly extracted from code arrives as an empty entry (`\"%lld Guests\" : { }`). Until someone varies it — in the catalog editor, or by hand as above — every count renders that one string. Xcode's shipped reference states the same rule: \"If `StringCatalogContext` returned a `sourcePluralCasesToAdd`, the source string might have to be varied by plural, but is not yet.\"\n\n### XLIFF Export Format\n\nWhen exporting for translation (File → Export Localizations):\n\n**Legacy (stringsdict)**:\n```xml\n\n %#@recentVisitors@\n\n\n\n %lld Recent Visitor\n %lld Visitante Recente\n\n```\n\n**String Catalog (cleaner)**:\n```xml\n\n %lld Recent Visitor\n %lld Visitante Recente\n\n\n\n %lld Recent Visitors\n %lld Visitantes Recentes\n\n```\n\n### Substitutions with Plural Variables\n\n```swift\n// Multiple variables with different plural forms\nlet message = String(localized: \"\\(songCount) songs on \\(albumCount) albums\")\n```\n\nOne entry holds one substitution per argument — not a combination per pair. Each substitution names the argument it formats (`argNum`, 1-based) and its specifier, and the entry's `stringUnit` is a template referring to the substitutions by name:\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"%lld songs on %lld albums\" : {\n \"localizations\" : {\n \"en\" : {\n \"substitutions\" : {\n \"songs\" : {\n \"argNum\" : 1,\n \"formatSpecifier\" : \"lld\",\n \"variations\" : {\n \"plural\" : {\n \"one\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld song\" } },\n \"other\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld songs\" } }\n }\n }\n },\n \"albums\" : {\n \"argNum\" : 2,\n \"formatSpecifier\" : \"lld\",\n \"variations\" : {\n \"plural\" : {\n \"one\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld album\" } },\n \"other\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld albums\" } }\n }\n }\n }\n },\n \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%#@songs@ on %#@albums@\" }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\nThat compiles to a single stringsdict entry whose `NSStringLocalizedFormatKey` is `%1$#@songs@ on %2$#@albums@`, with `songs` and `albums` each declaring their own plural cases. XLIFF export emits one unit per substitution per category under the same entry id (`%lld songs on %lld albums|==|substitutions.songs.plural.one`, `…|==|substitutions.albums.plural.one`, …) — an Arabic catalog needs 12 such units (2 arguments × 6 categories), not 4 combined entries.\n\n---\n\n## Part 5: Device & Width Variations\n\n### Device-Specific Strings\n\nDifferent text for different platforms:\n\n```swift\n// Same code, different strings per device\nText(\"Bird Food Shop\")\n```\n\n**String Catalog variations**:\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Bird Food Shop\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"device\" : {\n \"applewatch\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Bird Food\"\n }\n },\n \"other\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Bird Food Shop\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n**Result**:\n- iPhone/iPad: \"Bird Food Shop\"\n- Apple Watch: \"Bird Food\" (shorter for small screen)\n\n### Width Variations\n\nAdaptive strings for a specific presentation width — Apple's `NSBundle.h` documentation describes the feature as providing \"text that avoids truncation and maximizes available space\". The axis takes **numeric widths**, not size-class names:\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Application Settings\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"width\" : {\n \"1\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Settings\"\n }\n },\n \"200\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Application Settings\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n`\"compact\"`, `\"regular\"`, `\"short\"` and friends are rejected by the catalog compiler (`error: 'compact' is invalid for the type 'Width'`); a number compiles to a `NSStringVariableWidthRuleType` table. This axis is **not** how Dynamic Type or size classes are handled — use text styles, Auto Layout, and `ViewThatFits` for those.\n\nStandard UIKit controls select the variant for the current screen size themselves. Per the same header: \"Don't call this method when setting user-visible text for standard UIKit controls, such as `UILabel`. UIKit provides built-in support for adaptive strings, and automatically selects the string width variant appropriate for the current screen size.\" Everywhere else you select it yourself, passing the width:\n\n```swift\nlet widthAware = (template as NSString).variantFittingPresentationWidth(200)\n```\n\n---\n\n## Part 6: RTL Support\n\n### Layout Mirroring\n\nSwiftUI automatically mirrors layouts for RTL languages:\n\n```swift\n// ✅ Automatically mirrors for Arabic/Hebrew\nHStack {\n Image(systemName: \"chevron.right\")\n Text(\"Next\")\n}\n\n// iPhone (English): [>] Next\n// iPhone (Arabic): Next [<]\n```\n\n### Leading/Trailing vs Left/Right\n\n**Always use semantic directions**:\n\n```swift\n// ✅ Correct - mirrors automatically\n.padding(.leading, 16)\n.frame(maxWidth: .infinity, alignment: .leading)\n\n// ❌ Wrong - doesn't mirror\n.padding(.left, 16)\n.frame(maxWidth: .infinity, alignment: .left)\n```\n\n### Images and Icons\n\nMark images that should/shouldn't flip:\n\n```swift\n// ✅ Directional - mirrors for RTL\nImage(systemName: \"chevron.forward\")\n\n// ✅ Non-directional - never mirrors\nImage(systemName: \"star.fill\")\n\n// Custom images\nImage(\"backButton\")\n .flipsForRightToLeftLayoutDirection(true)\n```\n\n### Testing in RTL Mode\n\n**Xcode Scheme**:\n1. Edit Scheme → Run → Options\n2. Application Language: Arabic / Hebrew\n3. OR: App Language → Right-to-Left Pseudolanguage\n\n**Simulator**:\nSettings → General → Language & Region → Preferred Language Order\n\n**SwiftUI Preview**:\n```swift\nstruct ContentView_Previews: PreviewProvider {\n static var previews: some View {\n ContentView()\n .environment(\\.layoutDirection, .rightToLeft)\n .environment(\\.locale, Locale(identifier: \"ar\"))\n }\n}\n```\n\n---\n\n## Part 7: Locale-Aware Formatting\n\n### DateFormatter\n\n```swift\nlet formatter = DateFormatter()\nformatter.locale = Locale.current // ✅ Use current locale\nformatter.dateStyle = .long\nformatter.timeStyle = .short\n\nlet dateString = formatter.string(from: Date())\n\n// US: \"January 15, 2024 at 3:30 PM\"\n// France: \"15 janvier 2024 à 15:30\"\n// Japan: \"2024年1月15日 15:30\"\n```\n\n**Never hardcode date format strings**:\n```swift\n// ❌ Wrong - breaks in other locales\nformatter.dateFormat = \"MM/dd/yyyy\"\n\n// ✅ Correct - adapts to locale\nformatter.dateStyle = .short\n```\n\n### NumberFormatter for Currency\n\n```swift\nlet formatter = NumberFormatter()\nformatter.locale = Locale.current\nformatter.numberStyle = .currency\n\nlet priceString = formatter.string(from: 29.99)\n\n// US: \"$29.99\"\n// UK: \"£29.99\"\n// Japan: \"¥30\" (rounds to integer)\n// France: \"29,99 €\" (comma decimal, space before symbol)\n```\n\n### MeasurementFormatter\n\n```swift\nlet distance = Measurement(value: 100, unit: UnitLength.meters)\n\nlet formatter = MeasurementFormatter()\nformatter.locale = Locale.current\n\nlet distanceString = formatter.string(from: distance)\n\n// en_US: \"0.062 mi\" — the locale's natural unit for 100 m, not the one you passed\n// fr_FR: \"0,1 km\"\n\n// Keep the unit you passed instead of the locale's natural scale:\nformatter.unitOptions = .providedUnit\n// en_US and fr_FR both: \"100 m\"\n```\n\n### Locale-Specific Sorting\n\n```swift\nlet names = [\"Ångström\", \"Zebra\", \"Apple\"]\n\n// ✅ Locale-aware sort\nlet sorted = names.sorted { (lhs, rhs) in\n lhs.localizedStandardCompare(rhs) == .orderedAscending\n}\n\n// sv_SE: [\"Apple\", \"Zebra\", \"Ångström\"] (Å sorts after Z in Swedish)\n// en_US: [\"Ångström\", \"Apple\", \"Zebra\"] (Å treated as A)\n```\n\n---\n\n## Part 8: App Shortcuts Localization\n\n### Phrases with Parameters\n\n```swift\nimport AppIntents\n\nstruct ShowTopDonutsIntent: AppIntent {\n static let title: LocalizedStringResource = \"Show Top Donuts\"\n\n @Parameter(title: \"Timeframe\")\n var timeframe: Timeframe\n\n static var parameterSummary: some ParameterSummary {\n Summary(\"Trends for \\(\\.$timeframe)\") {\n \\.$timeframe\n }\n }\n}\n```\n\n**String Catalog automatically extracts**:\n- Intent title\n- Parameter names\n- Phrase templates with placeholders\n\n**Localized phrases**:\n```\nEnglish: \"Food Truck Trends for this week\"\nSpanish: \"Tendencias de Food Truck para esta semana\"\n```\n\n### AppShortcutsProvider Localization\n\n```swift\nstruct FoodTruckShortcuts: AppShortcutsProvider {\n static var appShortcuts: [AppShortcut] {\n AppShortcut(\n intent: ShowTopDonutsIntent(),\n phrases: [\n \"\\(.applicationName) Trends for \\(\\.$timeframe)\",\n \"Show trending donuts for \\(\\.$timeframe) in \\(.applicationName)\",\n \"Give me trends for \\(\\.$timeframe) in \\(.applicationName)\"\n ]\n )\n }\n}\n```\n\nXcode extracts all 3 phrases into String Catalog for translation.\n\n---\n\n## Part 9: Migration from Legacy\n\n### Converting .strings to .xcstrings\n\n**Automatic migration**:\n1. Select `.strings` file in Navigator\n2. Editor → Convert to String Catalog\n3. Xcode creates `.xcstrings` and preserves translations\n\n**Manual approach**:\n1. Create new String Catalog\n2. Build project (Xcode extracts strings from code)\n3. Import translations via File → Import Localizations (XLIFF)\n4. Delete old `.strings` files\n\n### Converting .stringsdict\n\n**Plural files automatically merge**:\n1. Keep `.strings` and `.stringsdict` together\n2. Convert → Both merge into single `.xcstrings`\n3. Plural variations preserved\n\n### Gradual Migration Strategy\n\n**Phase 1**: New code uses String Catalogs\n- Create `Localizable.xcstrings`\n- Write new code with `String(localized:)`\n- Keep legacy `.strings` files for old code\n\n**Phase 2**: Migrate existing strings\n- Convert one `.strings` table at a time\n- Test translations after each conversion\n- Update code using old `NSLocalizedString` calls\n\n**Phase 3**: Remove legacy files\n- Delete `.strings` and `.stringsdict` files\n- Verify all strings in String Catalog\n- Submit to App Store\n\n**Coexistence**: a legacy table and a `.xcstrings` table can live in the same target side by side, but **not under the same name**. `Localizable.xcstrings` beside `Localizable.strings`/`Localizable.stringsdict` fails the localization build: \"Localizable.xcstrings cannot co-exist with other .strings or .stringsdict tables with the same name.\" Keep the legacy file's name distinct (for example `Legacy.strings`) for as long as both exist — a differently named table is exported as its own `` element.\n\n---\n\n## Common Mistakes\n\n### Hardcoded Strings\n\n```swift\n// ❌ Wrong - not localizable\nText(\"Welcome\")\nlet title = \"Settings\"\n\n// ✅ Correct - localizable\nText(\"Welcome\") // SwiftUI auto-localizes\nlet title = String(localized: \"Settings\")\n```\n\n### Concatenating Localized Strings\n\n```swift\n// ❌ Wrong - word order varies by language\nlet message = String(localized: \"You have\") + \" \\(count) \" + String(localized: \"items\")\n\n// ✅ Correct - single localizable string with substitution\nlet message = String(localized: \"You have \\(count) items\")\n```\n\n**Why wrong**: Some languages put numbers before nouns, some after.\n\n### Missing Plural Forms\n\n```swift\n// ❌ Wrong - grammatically incorrect for many languages\nText(\"\\(count) item(s)\")\n\n// ✅ Correct - proper plural handling\nText(\"\\(count) items\") // key \"%lld items\"; the one/other variation is added in the catalog\n```\n\nExtraction gets you the key — `%lld items` — and nothing more. Add its plural variation in the catalog (Part 4); an unvaried entry ships one form for every count.\n\n### Ignoring RTL\n\n```swift\n// ❌ Wrong - breaks in RTL languages\n.padding(.left, 20)\nHStack {\n backButton\n Spacer()\n title\n}\n\n// ✅ Correct - mirrors automatically\n.padding(.leading, 20)\nHStack {\n backButton // Appears on right in RTL\n Spacer()\n title\n}\n```\n\n### Wrong Date/Number Formats\n\n```swift\n// ❌ Wrong - US-only format\nlet formatter = DateFormatter()\nformatter.dateFormat = \"MM/dd/yyyy\"\n\n// ✅ Correct - adapts to locale\nformatter.dateStyle = .short\nformatter.locale = Locale.current\n```\n\n### Forgetting Comments\n\n```swift\n// ❌ Wrong - translator has no context\nString(localized: \"Confirm\")\n\n// ✅ Correct - clear context\nString(localized: \"Confirm\", comment: \"Button to confirm delete action\")\n```\n\n**Impact**: \"Confirm\" could mean \"verify\" or \"acknowledge\" - context matters for accurate translation.\n\n---\n\n## Troubleshooting\n\n### Strings not appearing in String Catalog\n\n**Cause**: The source file isn't in the target, or extraction never ran for it\n\n**Solution**:\n1. Build Settings → \"Use Compiler to Extract Swift Strings\" (`SWIFT_EMIT_LOC_STRINGS`) → Yes. This changes *how* strings are extracted, not whether: with it off they still land in the catalog, but SwiftUI interpolations are recorded under legacy `%@` keys (`%@ Recent Visitors`) instead of typed `%lld` keys — and only a typed key can carry a plural variation.\n2. Clean Build Folder (Cmd+Shift+K)\n3. Build project\n\n### Translations not showing in app\n\n**Cause 1**: Language not added to project\n1. Project → Info → Localizations → + button\n2. Add target language\n\n**Cause 2**: String marked as \"Stale\"\n- Remove stale strings or verify code still uses them\n\n### Plural forms incorrect\n\n**Cause**: The plural variation was never created — the key exists in the catalog but carries no `one`/`other` (or `%#@name@`) rules\n\n**Solution**: Vary the key in the catalog (Part 4). `String.localizedStringWithFormat` is the legacy equivalent, and it inflects only when its format string is backed by a plural rule in a stringsdict:\n```swift\n// ✅ Correct\nText(\"\\(count) items\")\n\n// ❌ Wrong\nText(String.localizedStringWithFormat(NSLocalizedString(\"%d items\", comment: \"\"), count))\n```\n\n### XLIFF export missing strings\n\n**Cause**: There is nothing to export — the strings were never extracted (see above), usually because the source file isn't in the target\n\n**Solution**:\n1. Build Settings → \"Use Compiler to Extract Swift Strings\" → Yes\n2. Export Localizations again\n\n**Not the cause**: \"Localization Prefers String Catalogs\" (`LOCALIZATION_PREFERS_STRING_CATALOGS`) chooses the format of string tables *generated during an export* — it is what decides whether a generated table is exported as `-InfoPlist.xcstrings` or as `en.lproj/-InfoPlist.strings`. Two exports of the same project with it on and off produce identical `Localizable` trans-units.\n\n### Generated symbols not appearing (Xcode 26+)\n\n**Cause 1**: Build setting not enabled\n\n**Solution**:\n1. Build Settings → \"Generate String Catalog Symbols\" → Yes\n2. Clean Build Folder (Cmd+Shift+K)\n3. Rebuild project\n\n**Cause 2**: String not manually added to catalog\n\n**Solution**: Symbols only generate for manually-added strings (+ button in String Catalog). Auto-extracted strings don't generate symbols.\n\n### #bundle macro not working (Xcode 26+)\n\n**Cause**: Wrong syntax or missing import\n\n**Solution**:\n```swift\nimport Foundation // Required for #bundle\nText(\"My Collections\", bundle: #bundle, comment: \"Section title\")\n```\n\nVerify you're using `#bundle` not `.module`.\n\n### Refactoring to symbols fails (Xcode 26+)\n\n**Cause 1**: String not in String Catalog\n1. Ensure string exists in `.xcstrings` file\n2. Build project to refresh catalog\n3. Try refactoring again\n\n**Cause 2**: Build setting not enabled\n- Enable \"Generate String Catalog Symbols\" in Build Settings\n- Clean and rebuild\n\n---\n\n## Part 10: Xcode 26 Localization Enhancements\n\nXcode 26 introduces type-safe localization with generated symbols, automatic comment generation using on-device AI, and improved Swift Package support with the `#bundle` macro. Based on WWDC 2025 session 225 \"Explore localization with Xcode\".\n\n### Generated Symbols (Type-Safe Localization)\n\n**The problem**: String-based localization fails silently when typos occur.\n\n```swift\n// ❌ Typo - fails silently at runtime\nText(\"App.HomeScren.Title\") // Missing 'e' in Screen\n```\n\n**The solution**: Xcode 26 generates type-safe symbols from manually-added strings.\n\n#### How It Works\n\n1. **Add strings manually** to String Catalog using the + button\n2. **Enable build setting**: \"Generate String Catalog Symbols\" → Yes (the setting defaults to NO)\n3. **Use symbols** instead of strings\n\n```swift\n// ✅ Type-safe - compiler catches typos\nText(.appHomeScreenTitle)\n```\n\n#### Symbol Generation Rules\n\n| String Type | Generated Symbol Type | Usage Example |\n|-------------|----------------------|---------------|\n| No placeholders | Static property | `Text(.introductionTitle)` |\n| With placeholders | Function with positional (unlabeled) arguments | `.youHaveFriends(42)` |\n\n**Key naming conversion**:\n- `App.HomeScreen.Title` → `.appHomeScreenTitle`\n- Periods removed, camel-cased\n- Available on `LocalizedStringResource`\n\nPlaceholder arguments are **positional**, not labeled, and take their types from the specifiers — `generate-symbols` writes:\n\n```swift\nstatic func youHaveFriends(_ arg1: Int) -> LocalizedStringResource\nstatic func sentMessages(_ arg1: String, _ arg2: Int) -> LocalizedStringResource\n```\n\nso `.youHaveFriends(42)` compiles and `.youHaveFriends(arg1: 42)` does not (`error: extraneous argument label 'arg1:' in call`). A catalog's `argumentNames` array does not change this — it is metadata, not a parameter name.\n\n#### Code Examples\n\n```swift\n// SwiftUI views\n@available(iOS 26, *) // navigationSubtitle(_:) is iOS 26+\nstruct ContentView: View {\n var body: some View {\n NavigationStack {\n Text(.introductionTitle)\n .navigationSubtitle(.subtitle(42))\n }\n }\n}\n\n// Foundation String\nlet message = String(localized: .curatedCollection)\n\n// Custom views with LocalizedStringResource\nstruct CollectionDetailEditingView: View {\n let title: LocalizedStringResource\n\n init(title: LocalizedStringResource) {\n self.title = title\n }\n\n var body: some View {\n Text(title)\n }\n}\n\nCollectionDetailEditingView(title: .editingTitle)\n```\n\n---\n\n### Automatic Comment Generation\n\nXcode 26 uses an **on-device model** to automatically generate contextual comments for localizable strings.\n\n#### Enabling the Feature\n\n1. Open Xcode Settings → Editing\n2. Enable \"automatically generate string catalog comments\"\n3. New strings added to code automatically receive generated comments\n\n#### Example\n\nFor a button string, Xcode generates:\n\n> \"The text label on a button to cancel the deletion of a collection\"\n\nThis context helps translators understand where and how the string is used.\n\n#### XLIFF Export\n\nAuto-generated comments are marked in exported XLIFF files:\n\n```xml\n\n Grand Canyon\n Grand Canyon\n Suggestion for searching landmarks\n\n```\n\n**Benefits**:\n- Saves developer time writing translator context\n- Provides consistent, clear descriptions\n- Improves translation quality\n\n---\n\n### Swift Package & Framework Localization\n\n#### The Problem\n\nSwiftUI uses the `.main` bundle by default. Swift Packages and frameworks need to reference their own bundle:\n\n```swift\n// ❌ Wrong - uses main bundle, strings not found\nText(\"My Collections\", comment: \"Section title\")\n```\n\n#### The Solution: #bundle Macro (NEW in Xcode 26)\n\nThe `#bundle` macro automatically references the correct bundle for the current target:\n\n```swift\n// ✅ Correct - automatically uses package/framework bundle\nText(\"My Collections\", bundle: #bundle, comment: \"Section title\")\n```\n\n**Key advantages**:\n- Works in main app, frameworks, and Swift Packages\n- Backwards-compatible with older OS versions\n- Eliminates manual `.module` bundle management\n\n#### With Custom Table Names\n\n```swift\n// Main app\nText(\"My Collections\",\n tableName: \"Discover\",\n comment: \"Section title\")\n\n// Framework or Swift Package\nText(\"My Collections\",\n tableName: \"Discover\",\n bundle: #bundle,\n comment: \"Section title\")\n```\n\n---\n\n### Custom Table Symbol Access\n\nWhen using multiple String Catalogs for organization:\n\n#### Default \"Localizable\" Table\n\nSymbols are directly accessible on `LocalizedStringResource`:\n\n```swift\nText(.welcomeMessage) // From Localizable.xcstrings\n```\n\n**Note**: Xcode automatically resolves symbols from the default \"Localizable\" table. Explicit table selection is rarely needed—use it only for debugging or testing specific catalogs.\n\n#### Custom Tables\n\nSymbols are nested in the table namespace:\n\n```swift\n// From Discover.xcstrings\nText(.Discover.featuredCollection)\n\n// From Settings.xcstrings\nText(.Settings.privacyPolicy)\n```\n\n**Organization strategy for large apps**:\n- **Localizable.xcstrings** - Core app strings\n- **FeatureName.xcstrings** - Feature-specific strings (e.g., Onboarding, Settings, Discover)\n- Benefits: Easier to manage, clearer ownership, better XLIFF organization\n\n---\n\n### Two Localization Workflows\n\nXcode 26 supports two complementary workflows:\n\n#### Workflow 1: String Extraction (Recommended for new projects)\n\n**Process**:\n1. Write strings directly in code\n2. Use SwiftUI views (`Text`, `Button`) and `String(localized:)`\n3. Xcode automatically extracts to String Catalog\n4. Leverage automatic comment generation\n\n**Pros**: Simple initial setup, immediate start\n\n**Cons**: Less control over string organization\n\n```swift\n// ✅ String extraction workflow\nText(\"Welcome to WWDC!\", comment: \"Main welcome message\")\n```\n\n#### Workflow 2: Generated Symbols (Recommended as complexity grows)\n\n**Process**:\n1. Manually add strings to String Catalog\n2. Reference via type-safe symbols\n3. Organize into custom tables\n\n**Pros**: Better control, type safety, easier to maintain across frameworks\n\n**Cons**: Requires planning string catalog structure upfront\n\n```swift\n// ✅ Generated symbols workflow\nText(.welcomeMessage)\n```\n\n| Workflow | Best For | Trade-offs |\n|----------|----------|------------|\n| String Extraction | New projects, simple apps, prototyping | Automatic extraction, less control over organization |\n| Generated Symbols | Large apps, frameworks, multiple teams | Type safety, better organization, requires upfront planning |\n\n---\n\n### Refactoring Between Workflows\n\nXcode 26 allows converting between workflows without manual rewriting.\n\n#### Converting Strings to Symbols\n\n1. **Right-click** on a string literal in code\n2. Select **\"Refactor > Convert Strings to Symbols\"**\n3. **Preview** all affected locations\n4. **Customize** symbol names before confirming\n5. **Apply** to entire table or individual strings\n\n**Example**:\n\n```swift\n// Before\nText(\"Welcome to WWDC!\", comment: \"Main welcome message\")\n\n// After refactoring\nText(.welcomeToWWDC)\n```\n\n**Benefits**:\n- Batch conversion of entire String Catalogs\n- Preview changes before applying\n- Maintain localization without code rewrites\n\n---\n\n### Implementation Checklist\n\nAfter adopting Xcode 26 generated symbols, verify:\n\n**Build Configuration:**\n- [ ] \"Generate String Catalog Symbols\" build setting enabled\n- [ ] Project builds without \"Cannot find 'symbolName' in scope\" errors\n- [ ] Clean build succeeds (Cmd+Shift+K, then Cmd+B)\n\n**String Catalog Setup:**\n- [ ] Strings manually added to catalog using + button (not auto-extracted)\n- [ ] Symbol names follow conventions (camelCase, no periods)\n- [ ] Custom tables organized by feature (if using multiple catalogs)\n\n**Swift Package Integration:**\n- [ ] All `Text()` and `String(localized:)` calls in packages use `bundle: #bundle`\n- [ ] Import Foundation added where `#bundle` is used\n- [ ] Tested package builds independently and as dependency\n\n**Refactoring & Migration:**\n- [ ] Tested refactoring tool on sample strings\n- [ ] Preview showed expected changes before applying\n- [ ] Old string-based calls still work during transition period\n\n**Optional Features:**\n- [ ] Automatic comment generation enabled in Xcode Settings → Editing (optional)\n- [ ] Tested AI-generated comments for accuracy\n- [ ] XLIFF export includes auto-generated comments\n\n**Testing:**\n- [ ] Symbols resolve correctly in SwiftUI previews\n- [ ] Localization works across all supported languages\n- [ ] App runs on minimum supported iOS version\n\n---\n\n## Resources\n\n**WWDC**: 2025-225, 2023-10155, 2022-10110\n\n**Docs**: /xcode/localization, /xcode/localizing-and-varying-text-with-a-string-catalog\n\n**Skills**: skills/localization-research-ref.md (Apple terminology matching, glossary, VoiceOver comments, pseudolocalization, TMS), skills/app-intents-ref.md, axiom-design (skills/hig.md), axiom-accessibility\n", + "content": "\n# Localization & Internationalization\n\nComprehensive guide to app localization using String Catalogs. Apple Design Award Inclusivity winners always support multiple languages with excellent RTL (Right-to-Left) support.\n\n## Overview\n\nString Catalogs (`.xcstrings`) are Xcode 15's unified format for managing app localization. They replace legacy `.strings` and `.stringsdict` files with a single JSON-based format that's easier to maintain, diff, and integrate with translation workflows.\n\nThis skill covers String Catalogs, SwiftUI/UIKit localization APIs, plural handling, RTL support, locale-aware formatting, and migration strategies from legacy formats.\n\n## When to Use This Skill\n\n- Setting up String Catalogs in Xcode 15+\n- Localizing SwiftUI and UIKit apps\n- Handling plural forms correctly (critical for many languages)\n- Supporting RTL languages (Arabic, Hebrew)\n- Formatting dates, numbers, and currencies by locale\n- Migrating from legacy `.strings`/`.stringsdict` files\n- Preparing App Shortcuts and App Intents for localization\n- Debugging missing translations or incorrect plural forms\n\n## System Requirements\n\n- **Xcode 15+** for String Catalogs (`.xcstrings`)\n- **Xcode 26+** for automatic symbol generation, `#bundle` macro, and AI-powered comment generation\n- **iOS 16+** for `LocalizedStringResource` (`@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)`)\n- **iOS 16+** for App Shortcuts localization\n- Earlier iOS versions use legacy `.strings` files\n\n---\n\n## Part 1: String Catalogs (WWDC 2023/10155)\n\n### Creating a String Catalog\n\n**Method 1: Xcode Navigator**\n1. File → New → File\n2. Choose \"String Catalog\"\n3. Name it (e.g., `Localizable.xcstrings`)\n4. Add to target\n\n**Method 2: Automatic Extraction**\n\nXcode 15 can automatically extract strings from:\n- SwiftUI views (string literals in `Text`, `Label`, `Button`)\n- Swift code (`String(localized:)`)\n- Objective-C (`NSLocalizedString`)\n- C (`CFCopyLocalizedString`)\n- Interface Builder files (`.storyboard`, `.xib`)\n- Info.plist values\n- App Shortcuts phrases\n\n**Build Settings Required**:\n- **\"Use Compiler to Extract Swift Strings\"** → Yes\n- **\"Localization Prefers String Catalogs\"** → Yes\n\n### String Catalog Structure\n\nEach entry has:\n- **Key**: Unique identifier (default: the English string)\n- **Default Value**: Fallback if translation missing\n- **Comment**: Context for translators\n- **String Table**: Organization container (default: \"Localizable\")\n\n**Example `.xcstrings` JSON**:\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Thanks for shopping with us!\" : {\n \"comment\" : \"Label above checkout button\",\n \"localizations\" : {\n \"en\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Thanks for shopping with us!\"\n }\n },\n \"es\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"¡Gracias por comprar con nosotros!\"\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n### Two key styles — and what a tool must read\n\nAn entry's key is either the English source text itself or a symbolic identifier. The first entry below has the English as its key; the second uses a symbolic key and carries the English in an explicit source unit.\n\n```json\n\"Songs\" : {\n\n },\n\"quality.high\" : {\n \"comment\" : \"Playback quality: 256 kbps\",\n \"extractionState\" : \"extracted_with_value\",\n \"localizations\" : { \"en\" : { \"stringUnit\" : { \"state\" : \"new\", \"value\" : \"High\" } } }\n}\n```\n\n`String(localized: \"Songs\")` produces the first; `String(localized: \"quality.high\", defaultValue: \"High\")` produces the second, with `extracted_with_value` and an explicit source unit. So **the source string is `localizations[sourceLanguage]` if present, else the key** — a tool has to handle both, not pick one. (`sourceLanguage` is a top-level field and is not always `en`: in a catalog whose source is German the unit lands under `de`, so a tool that hardcodes `en` reads a translation as if it were the source.) A tool that reads only the source localization skips every key-as-English entry, and a search over keys alone misses every symbolic one.\n\n### Writing a catalog from a tool\n\nXcode has one byte layout, and a generic JSON writer rewrites the whole file — measured on a real 970-key catalog, `json.dump(..., indent=2)` produced a **26,531-line diff** that buried the actual translation. The layout:\n\n| Rule | Value |\n|---|---|\n| Indent | 2 spaces |\n| Key separator | `\" : \"` — a space *before* the colon |\n| Encoding | literal UTF-8, never `\\u` escapes |\n| Trailing newline | none |\n| Empty entry | `{`, a blank line, then `}` at the entry's own indent |\n| Keys inside an entry | sorted by code point |\n| Keys of the top-level `strings` map | two writers, two orders — see below |\n\n**The two Apple writers order the top-level keys differently.** `xcstringstool` sorts by code point. The Xcode editor's order matches Foundation's `localizedStandardCompare`, which is punctuation-aware: measured across two editor-written revisions of the same 1,086- and 1,447-key catalog, that comparison had 0 violations while plain code-point order had 139 and 212. So running `xcstringstool sync` over an editor-maintained catalog re-sorts every key — a 16,005-line diff on a file whose meaning did not change. Match whichever order the file already uses.\n\n**`xcstringstool` ships with Xcode** and is the headless path in and out of a catalog. It is not on `PATH`; run it through `xcrun`:\n\n```bash\nxcrun xcstringstool extract --SwiftUI --modern-localizable-strings -o /tmp/sd $(find Sources -name '*.swift')\nxcrun xcstringstool sync Localizable.xcstrings --stringsdata /tmp/sd/*.stringsdata --skip-marking-strings-stale\nxcrun xcstringstool print Localizable.xcstrings # every key\n```\n\n(`Sources/**/*.swift` is not a substitute for that `find`: macOS ships bash 3.2, which has no `globstar`, so `**` silently matches one directory level.)\n\n**`sync` prunes.** It is not an additive merge: every entry missing from the `.stringsdata` is marked `extractionState: stale` if it has translations and **deleted outright if it has none** — exit 0, no warning. Measured on a two-entry catalog: the untranslated entry vanished and the translated one came back stale. `--skip-marking-strings-stale` is the guard, and with it a 1,447-key catalog synced intact (122 untranslated entries all survived).\n\nThat makes the filename matter twice over: `sync` matches a `.stringsdata` table to the catalog **by filename**, so `Localizable.xcstrings` takes the `Localizable` table. Point it at `Strings.xcstrings` and nothing matches — so nothing is added, and without the guard flag every entry is pruned or staled.\n\n`sync` re-emits the canonical layout only when it has something to write; a pure reformat with no source change is left exactly as it is, so it is not a repair tool for a bad write.\n\nTo write translations — which `sync` does not do — parse, mutate only the entries you mean to touch, and re-emit in the layout above, ordering the top-level keys the way the file already orders them. In JavaScript, `JSON.parse` also hoists integer-like keys (`\"30\"`) to the front, so a parse/stringify round-trip reorders the file on its own; parse into an order-preserving structure instead.\n\n### Translation States\n\nXcode tracks state for each translation:\n\n- **New** (⚪) - String hasn't been translated yet\n- **Needs Review** (🟡) - Source changed, translation may be outdated\n- **Reviewed** (✅) - Translation approved and current\n- **Stale** (🔴) - String no longer found in source code\n\nIn the file itself the `state` values are `new`, `needs_review` and `translated` — \"Reviewed\" is the UI's word for `translated`. Staleness is not a `state`: Xcode writes it to `extractionState` as `stale`.\n\n**Workflow**:\n1. Developer adds string → **New**\n2. Translator adds translation → **Reviewed**\n3. Developer changes source → **Needs Review**\n4. Translator updates → **Reviewed**\n5. Developer removes code → **Stale**\n\n---\n\n## Part 2: SwiftUI Localization\n\n### LocalizedStringKey (Automatic)\n\nSwiftUI views with `String` parameters automatically support localization:\n\n```swift\n// ✅ Automatically localizable\nText(\"Welcome to WWDC!\")\nLabel(\"Thanks for shopping with us!\", systemImage: \"bag\")\nButton(\"Checkout\") { }\n\n// Xcode extracts these strings to String Catalog\n```\n\n**How it works**: SwiftUI uses `LocalizedStringKey` internally, which looks up strings in String Catalogs.\n\n### String(localized:) with Comments\n\nFor explicit localization in Swift code:\n\n```swift\n// Basic\nlet title = String(localized: \"Welcome to WWDC!\")\n\n// With comment for translators\nlet title = String(localized: \"Welcome to WWDC!\",\n comment: \"Notification banner title\")\n\n// With custom table\nlet title = String(localized: \"Welcome to WWDC!\",\n table: \"WWDCNotifications\",\n comment: \"Notification banner title\")\n\n// With default value (key ≠ English text)\nlet title = String(localized: \"WWDC_NOTIFICATION_TITLE\",\n defaultValue: \"Welcome to WWDC!\",\n comment: \"Notification banner title\")\n```\n\n**Best practice**: Always include `comment` to give translators context.\n\n### LocalizedStringResource (Deferred Localization)\n\nFor passing localizable strings to other functions:\n\n```swift\nimport Foundation\n\nstruct CardView: View {\n let title: LocalizedStringResource\n let subtitle: LocalizedStringResource\n\n var body: some View {\n ZStack {\n RoundedRectangle(cornerRadius: 10.0)\n VStack {\n Text(title) // Resolved at render time\n Text(subtitle)\n }\n .padding()\n }\n }\n}\n\n// Usage\nCardView(\n title: \"Recent Purchases\",\n subtitle: \"Items you've ordered in the past week.\"\n)\n```\n\n**Key difference**: `LocalizedStringResource` defers lookup until used, allowing custom views to be fully localizable.\n\n### AttributedString with Markdown\n\n```swift\n// Markdown formatting is preserved across localizations\nlet subtitle = AttributedString(localized: \"**Bold** and _italic_ text\")\n```\n\n---\n\n## Part 3: UIKit & Foundation\n\n### NSLocalizedString Macro\n\n```swift\n// Basic\nlet title = NSLocalizedString(\"Recent Purchases\", comment: \"Button Title\")\n\n// With table\nlet title = NSLocalizedString(\"Recent Purchases\",\n tableName: \"Shopping\",\n comment: \"Button Title\")\n\n// With bundle\nlet title = NSLocalizedString(\"Recent Purchases\",\n tableName: nil,\n bundle: .main,\n value: \"\",\n comment: \"Button Title\")\n```\n\n### Bundle.localizedString\n\n```swift\nlet customBundle = Bundle(for: MyFramework.self)\nlet text = customBundle.localizedString(forKey: \"Welcome\",\n value: nil,\n table: \"MyFramework\")\n```\n\n### Custom Macros\n\n```objc\n// Objective-C\n#define MyLocalizedString(key, comment) \\\n [myBundle localizedStringForKey:key value:nil table:nil]\n```\n\n### Info.plist Localization\n\nLocalize app name, permissions, etc.:\n\n1. Select `Info.plist`\n2. Editor → Add Localization\n3. Create `InfoPlist.strings` for each language:\n\n```\n// InfoPlist.strings (Spanish)\n\"CFBundleName\" = \"Mi Aplicación\";\n\"NSCameraUsageDescription\" = \"La app necesita acceso a la cámara para tomar fotos.\";\n```\n\n---\n\n## Part 4: Pluralization\n\nDifferent languages have different plural rules:\n\n- **English**: 2 forms (one, other)\n- **Russian**: 4 forms (one, few, many, other)\n- **Polish**: 4 forms (one, few, many, other)\n- **Arabic**: 6 forms (zero, one, two, few, many, other)\n\nXcode's own table (`XCStringsParser.framework/Resources/variationConfigFile.plist`, 711 languages) is the authority for these sets — `ru` and `pl` both list `one, few, many, other`. What matters for a translation is the number of variations the catalog can hold, not the number of forms a language is usually described with.\n\n### SwiftUI Plural Handling\n\n```swift\n// The extractor writes a typed key; the plural variation is added in the catalog\nText(\"\\(count) items\") // key: \"%lld items\"\nText(\"\\(visitorCount) Recent Visitors\") // key: \"%lld Recent Visitors\"\n```\n\n**In String Catalog** (the two top-level keys `sourceLanguage` and `version` are required):\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"%lld Recent Visitors\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"plural\" : {\n \"one\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"%lld Recent Visitor\"\n }\n },\n \"other\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"%lld Recent Visitors\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n**Xcode does not create those variations for you.** Extraction records the key and stops: a `%lld` entry with no `variations` stays a flat `stringUnit`, and a string newly extracted from code arrives as an empty entry (`\"%lld Guests\" : { }`). Until someone varies it — in the catalog editor, or by hand as above — every count renders that one string. Xcode's shipped reference states the same rule: \"If `StringCatalogContext` returned a `sourcePluralCasesToAdd`, the source string might have to be varied by plural, but is not yet.\"\n\n### XLIFF Export Format\n\nWhen exporting for translation (File → Export Localizations):\n\n**Legacy (stringsdict)**:\n```xml\n\n %#@recentVisitors@\n\n\n\n %lld Recent Visitor\n %lld Visitante Recente\n\n```\n\n**String Catalog (cleaner)**:\n```xml\n\n %lld Recent Visitor\n %lld Visitante Recente\n\n\n\n %lld Recent Visitors\n %lld Visitantes Recentes\n\n```\n\n### Substitutions with Plural Variables\n\n```swift\n// Multiple variables with different plural forms\nlet message = String(localized: \"\\(songCount) songs on \\(albumCount) albums\")\n```\n\nOne entry holds one substitution per argument — not a combination per pair. Each substitution names the argument it formats (`argNum`, 1-based) and its specifier, and the entry's `stringUnit` is a template referring to the substitutions by name:\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"%lld songs on %lld albums\" : {\n \"localizations\" : {\n \"en\" : {\n \"substitutions\" : {\n \"songs\" : {\n \"argNum\" : 1,\n \"formatSpecifier\" : \"lld\",\n \"variations\" : {\n \"plural\" : {\n \"one\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld song\" } },\n \"other\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld songs\" } }\n }\n }\n },\n \"albums\" : {\n \"argNum\" : 2,\n \"formatSpecifier\" : \"lld\",\n \"variations\" : {\n \"plural\" : {\n \"one\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld album\" } },\n \"other\" : { \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%lld albums\" } }\n }\n }\n }\n },\n \"stringUnit\" : { \"state\" : \"translated\", \"value\" : \"%#@songs@ on %#@albums@\" }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\nThat compiles to a single stringsdict entry whose `NSStringLocalizedFormatKey` is `%1$#@songs@ on %2$#@albums@`, with `songs` and `albums` each declaring their own plural cases. XLIFF export emits one unit per substitution per category under the same entry id (`%lld songs on %lld albums|==|substitutions.songs.plural.one`, `…|==|substitutions.albums.plural.one`, …) — an Arabic catalog needs 12 such units (2 arguments × 6 categories), not 4 combined entries.\n\n---\n\n## Part 5: Device & Width Variations\n\n### Device-Specific Strings\n\nDifferent text for different platforms:\n\n```swift\n// Same code, different strings per device\nText(\"Bird Food Shop\")\n```\n\n**String Catalog variations**:\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Bird Food Shop\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"device\" : {\n \"applewatch\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Bird Food\"\n }\n },\n \"other\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Bird Food Shop\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n**Result**:\n- iPhone/iPad: \"Bird Food Shop\"\n- Apple Watch: \"Bird Food\" (shorter for small screen)\n\n### Width Variations\n\nAdaptive strings for a specific presentation width — Apple's `NSBundle.h` documentation describes the feature as providing \"text that avoids truncation and maximizes available space\". The axis takes **numeric widths**, not size-class names:\n\n```json\n{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"Application Settings\" : {\n \"localizations\" : {\n \"en\" : {\n \"variations\" : {\n \"width\" : {\n \"1\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Settings\"\n }\n },\n \"200\" : {\n \"stringUnit\" : {\n \"state\" : \"translated\",\n \"value\" : \"Application Settings\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"version\" : \"1.0\"\n}\n```\n\n`\"compact\"`, `\"regular\"`, `\"short\"` and friends are rejected by the catalog compiler (`error: 'compact' is invalid for the type 'Width'`); a number compiles to a `NSStringVariableWidthRuleType` table. This axis is **not** how Dynamic Type or size classes are handled — use text styles, Auto Layout, and `ViewThatFits` for those.\n\nStandard UIKit controls select the variant for the current screen size themselves. Per the same header: \"Don't call this method when setting user-visible text for standard UIKit controls, such as `UILabel`. UIKit provides built-in support for adaptive strings, and automatically selects the string width variant appropriate for the current screen size.\" Everywhere else you select it yourself, passing the width:\n\n```swift\nlet widthAware = (template as NSString).variantFittingPresentationWidth(200)\n```\n\n---\n\n## Part 6: RTL Support\n\n### Layout Mirroring\n\nSwiftUI automatically mirrors layouts for RTL languages:\n\n```swift\n// ✅ Automatically mirrors for Arabic/Hebrew\nHStack {\n Image(systemName: \"chevron.right\")\n Text(\"Next\")\n}\n\n// iPhone (English): [>] Next\n// iPhone (Arabic): Next [<]\n```\n\n### Leading/Trailing vs Left/Right\n\n**Always use semantic directions**:\n\n```swift\n// ✅ Correct - mirrors automatically\n.padding(.leading, 16)\n.frame(maxWidth: .infinity, alignment: .leading)\n\n// ❌ Wrong - doesn't mirror\n.padding(.left, 16)\n.frame(maxWidth: .infinity, alignment: .left)\n```\n\n### Images and Icons\n\nMark images that should/shouldn't flip:\n\n```swift\n// ✅ Directional - mirrors for RTL\nImage(systemName: \"chevron.forward\")\n\n// ✅ Non-directional - never mirrors\nImage(systemName: \"star.fill\")\n\n// Custom images\nImage(\"backButton\")\n .flipsForRightToLeftLayoutDirection(true)\n```\n\n### Testing in RTL Mode\n\n**Xcode Scheme**:\n1. Edit Scheme → Run → Options\n2. Application Language: Arabic / Hebrew\n3. OR: App Language → Right-to-Left Pseudolanguage\n\n**Simulator**:\nSettings → General → Language & Region → Preferred Language Order\n\n**SwiftUI Preview**:\n```swift\nstruct ContentView_Previews: PreviewProvider {\n static var previews: some View {\n ContentView()\n .environment(\\.layoutDirection, .rightToLeft)\n .environment(\\.locale, Locale(identifier: \"ar\"))\n }\n}\n```\n\n---\n\n## Part 7: Locale-Aware Formatting\n\n### DateFormatter\n\n```swift\nlet formatter = DateFormatter()\nformatter.locale = Locale.current // ✅ Use current locale\nformatter.dateStyle = .long\nformatter.timeStyle = .short\n\nlet dateString = formatter.string(from: Date())\n\n// US: \"January 15, 2024 at 3:30 PM\"\n// France: \"15 janvier 2024 à 15:30\"\n// Japan: \"2024年1月15日 15:30\"\n```\n\n**Never hardcode date format strings**:\n```swift\n// ❌ Wrong - breaks in other locales\nformatter.dateFormat = \"MM/dd/yyyy\"\n\n// ✅ Correct - adapts to locale\nformatter.dateStyle = .short\n```\n\n### NumberFormatter for Currency\n\n```swift\nlet formatter = NumberFormatter()\nformatter.locale = Locale.current\nformatter.numberStyle = .currency\n\nlet priceString = formatter.string(from: 29.99)\n\n// US: \"$29.99\"\n// UK: \"£29.99\"\n// Japan: \"¥30\" (rounds to integer)\n// France: \"29,99 €\" (comma decimal, space before symbol)\n```\n\n### MeasurementFormatter\n\n```swift\nlet distance = Measurement(value: 100, unit: UnitLength.meters)\n\nlet formatter = MeasurementFormatter()\nformatter.locale = Locale.current\n\nlet distanceString = formatter.string(from: distance)\n\n// en_US: \"0.062 mi\" — the locale's natural unit for 100 m, not the one you passed\n// fr_FR: \"0,1 km\"\n\n// Keep the unit you passed instead of the locale's natural scale:\nformatter.unitOptions = .providedUnit\n// en_US and fr_FR both: \"100 m\"\n```\n\n### Locale-Specific Sorting\n\n```swift\nlet names = [\"Ångström\", \"Zebra\", \"Apple\"]\n\n// ✅ Locale-aware sort\nlet sorted = names.sorted { (lhs, rhs) in\n lhs.localizedStandardCompare(rhs) == .orderedAscending\n}\n\n// sv_SE: [\"Apple\", \"Zebra\", \"Ångström\"] (Å sorts after Z in Swedish)\n// en_US: [\"Ångström\", \"Apple\", \"Zebra\"] (Å treated as A)\n```\n\n---\n\n## Part 8: App Shortcuts Localization\n\n### Phrases with Parameters\n\n```swift\nimport AppIntents\n\nstruct ShowTopDonutsIntent: AppIntent {\n static let title: LocalizedStringResource = \"Show Top Donuts\"\n\n @Parameter(title: \"Timeframe\")\n var timeframe: Timeframe\n\n static var parameterSummary: some ParameterSummary {\n Summary(\"Trends for \\(\\.$timeframe)\") {\n \\.$timeframe\n }\n }\n}\n```\n\n**String Catalog automatically extracts**:\n- Intent title\n- Parameter names\n- Phrase templates with placeholders\n\n**Localized phrases**:\n```\nEnglish: \"Food Truck Trends for this week\"\nSpanish: \"Tendencias de Food Truck para esta semana\"\n```\n\n### AppShortcutsProvider Localization\n\n```swift\nstruct FoodTruckShortcuts: AppShortcutsProvider {\n static var appShortcuts: [AppShortcut] {\n AppShortcut(\n intent: ShowTopDonutsIntent(),\n phrases: [\n \"\\(.applicationName) Trends for \\(\\.$timeframe)\",\n \"Show trending donuts for \\(\\.$timeframe) in \\(.applicationName)\",\n \"Give me trends for \\(\\.$timeframe) in \\(.applicationName)\"\n ]\n )\n }\n}\n```\n\nXcode extracts all 3 phrases into String Catalog for translation.\n\n---\n\n## Part 9: Migration from Legacy\n\n### Converting .strings to .xcstrings\n\n**Automatic migration**:\n1. Select `.strings` file in Navigator\n2. Editor → Convert to String Catalog\n3. Xcode creates `.xcstrings` and preserves translations\n\n**Manual approach**:\n1. Create new String Catalog\n2. Build project (Xcode extracts strings from code)\n3. Import translations via File → Import Localizations (XLIFF)\n4. Delete old `.strings` files\n\n### Converting .stringsdict\n\n**Plural files automatically merge**:\n1. Keep `.strings` and `.stringsdict` together\n2. Convert → Both merge into single `.xcstrings`\n3. Plural variations preserved\n\n### Gradual Migration Strategy\n\n**Phase 1**: New code uses String Catalogs\n- Create `Localizable.xcstrings`\n- Write new code with `String(localized:)`\n- Keep legacy `.strings` files for old code\n\n**Phase 2**: Migrate existing strings\n- Convert one `.strings` table at a time\n- Test translations after each conversion\n- Update code using old `NSLocalizedString` calls\n\n**Phase 3**: Remove legacy files\n- Delete `.strings` and `.stringsdict` files\n- Verify all strings in String Catalog\n- Submit to App Store\n\n**Coexistence**: a legacy table and a `.xcstrings` table can live in the same target side by side, but **not under the same name**. `Localizable.xcstrings` beside `Localizable.strings`/`Localizable.stringsdict` fails the localization build: \"Localizable.xcstrings cannot co-exist with other .strings or .stringsdict tables with the same name.\" Keep the legacy file's name distinct (for example `Legacy.strings`) for as long as both exist — a differently named table is exported as its own `` element.\n\n---\n\n## Common Mistakes\n\n### Hardcoded Strings\n\n```swift\n// ❌ Wrong - not localizable\nText(\"Welcome\")\nlet title = \"Settings\"\n\n// ✅ Correct - localizable\nText(\"Welcome\") // SwiftUI auto-localizes\nlet title = String(localized: \"Settings\")\n```\n\n### Concatenating Localized Strings\n\n```swift\n// ❌ Wrong - word order varies by language\nlet message = String(localized: \"You have\") + \" \\(count) \" + String(localized: \"items\")\n\n// ✅ Correct - single localizable string with substitution\nlet message = String(localized: \"You have \\(count) items\")\n```\n\n**Why wrong**: Some languages put numbers before nouns, some after.\n\n### Missing Plural Forms\n\n```swift\n// ❌ Wrong - grammatically incorrect for many languages\nText(\"\\(count) item(s)\")\n\n// ✅ Correct - proper plural handling\nText(\"\\(count) items\") // key \"%lld items\"; the one/other variation is added in the catalog\n```\n\nExtraction gets you the key — `%lld items` — and nothing more. Add its plural variation in the catalog (Part 4); an unvaried entry ships one form for every count.\n\n### Ignoring RTL\n\n```swift\n// ❌ Wrong - breaks in RTL languages\n.padding(.left, 20)\nHStack {\n backButton\n Spacer()\n title\n}\n\n// ✅ Correct - mirrors automatically\n.padding(.leading, 20)\nHStack {\n backButton // Appears on right in RTL\n Spacer()\n title\n}\n```\n\n### Wrong Date/Number Formats\n\n```swift\n// ❌ Wrong - US-only format\nlet formatter = DateFormatter()\nformatter.dateFormat = \"MM/dd/yyyy\"\n\n// ✅ Correct - adapts to locale\nformatter.dateStyle = .short\nformatter.locale = Locale.current\n```\n\n### Forgetting Comments\n\n```swift\n// ❌ Wrong - translator has no context\nString(localized: \"Confirm\")\n\n// ✅ Correct - clear context\nString(localized: \"Confirm\", comment: \"Button to confirm delete action\")\n```\n\n**Impact**: \"Confirm\" could mean \"verify\" or \"acknowledge\" - context matters for accurate translation.\n\n### Expecting a comment to separate two meanings\n\nA comment gives a translator context; it does **not** split a key. One English word used for two concepts is one entry with one translation, and the comments are merged:\n\n```swift\nString(localized: \"High\", comment: \"Energy level of a track\")\nString(localized: \"High\", comment: \"Playback quality: 256 kbps\")\n```\n\n```json\n\"High\" : { \"comment\" : \"Energy level of a track\\nPlayback quality: 256 kbps\" }\n```\n\n`xcstringstool sync` says so — `notice: Key \"High\" used with multiple comments: …` — and then the catalog ships whichever translation the translator picked for both. Give each meaning its own key and keep the English in a default value:\n\n```swift\nString(localized: \"energy.high\", defaultValue: \"High\", comment: \"Energy level of a track\")\nString(localized: \"quality.high\", defaultValue: \"High\", comment: \"Playback quality: 256 kbps\")\n```\n\nThis bites hardest where a short word is reused across features — a picker label and a Siri phrase, say — because the wrong sense only shows up in the other feature's UI. Before renaming any enum title or button to a short common word, search the catalog for that exact key.\n\n---\n\n## Troubleshooting\n\n### Strings not appearing in String Catalog\n\n**Cause**: The source file isn't in the target, or extraction never ran for it\n\n**Solution**:\n1. Build Settings → \"Use Compiler to Extract Swift Strings\" (`SWIFT_EMIT_LOC_STRINGS`) → Yes. This changes *how* strings are extracted, not whether: with it off they still land in the catalog, but SwiftUI interpolations are recorded under legacy `%@` keys (`%@ Recent Visitors`) instead of typed `%lld` keys — and only a typed key can carry a plural variation.\n2. Clean Build Folder (Cmd+Shift+K)\n3. Build project\n\n### Translations not showing in app\n\n**Cause 1**: Language not added to project\n1. Project → Info → Localizations → + button\n2. Add target language\n\n**Cause 2**: String marked as \"Stale\"\n- Remove stale strings or verify code still uses them\n\n### Plural forms incorrect\n\n**Cause**: The plural variation was never created — the key exists in the catalog but carries no `one`/`other` (or `%#@name@`) rules\n\n**Solution**: Vary the key in the catalog (Part 4). `String.localizedStringWithFormat` is the legacy equivalent, and it inflects only when its format string is backed by a plural rule in a stringsdict:\n```swift\n// ✅ Correct\nText(\"\\(count) items\")\n\n// ❌ Wrong\nText(String.localizedStringWithFormat(NSLocalizedString(\"%d items\", comment: \"\"), count))\n```\n\n### XLIFF export missing strings\n\n**Cause**: There is nothing to export — the strings were never extracted (see above), usually because the source file isn't in the target\n\n**Solution**:\n1. Build Settings → \"Use Compiler to Extract Swift Strings\" → Yes\n2. Export Localizations again\n\n**Not the cause**: \"Localization Prefers String Catalogs\" (`LOCALIZATION_PREFERS_STRING_CATALOGS`) chooses the format of string tables *generated during an export* — it is what decides whether a generated table is exported as `-InfoPlist.xcstrings` or as `en.lproj/-InfoPlist.strings`. Two exports of the same project with it on and off produce identical `Localizable` trans-units.\n\n### Generated symbols not appearing (Xcode 26+)\n\n**Cause 1**: Build setting not enabled\n\n**Solution**:\n1. Build Settings → \"Generate String Catalog Symbols\" → Yes\n2. Clean Build Folder (Cmd+Shift+K)\n3. Rebuild project\n\n**Cause 2**: String not manually added to catalog\n\n**Solution**: Symbols only generate for manually-added strings (+ button in String Catalog). Auto-extracted strings don't generate symbols.\n\n### #bundle macro not working (Xcode 26+)\n\n**Cause**: Wrong syntax or missing import\n\n**Solution**:\n```swift\nimport Foundation // Required for #bundle\nText(\"My Collections\", bundle: #bundle, comment: \"Section title\")\n```\n\nVerify you're using `#bundle` not `.module`.\n\n### Refactoring to symbols fails (Xcode 26+)\n\n**Cause 1**: String not in String Catalog\n1. Ensure string exists in `.xcstrings` file\n2. Build project to refresh catalog\n3. Try refactoring again\n\n**Cause 2**: Build setting not enabled\n- Enable \"Generate String Catalog Symbols\" in Build Settings\n- Clean and rebuild\n\n---\n\n## Part 10: Xcode 26 Localization Enhancements\n\nXcode 26 introduces type-safe localization with generated symbols, automatic comment generation using on-device AI, and improved Swift Package support with the `#bundle` macro. Based on WWDC 2025 session 225 \"Explore localization with Xcode\".\n\n### Generated Symbols (Type-Safe Localization)\n\n**The problem**: String-based localization fails silently when typos occur.\n\n```swift\n// ❌ Typo - fails silently at runtime\nText(\"App.HomeScren.Title\") // Missing 'e' in Screen\n```\n\n**The solution**: Xcode 26 generates type-safe symbols from manually-added strings.\n\n#### How It Works\n\n1. **Add strings manually** to String Catalog using the + button\n2. **Enable build setting**: \"Generate String Catalog Symbols\" → Yes (the setting defaults to NO)\n3. **Use symbols** instead of strings\n\n```swift\n// ✅ Type-safe - compiler catches typos\nText(.appHomeScreenTitle)\n```\n\n#### Symbol Generation Rules\n\n| String Type | Generated Symbol Type | Usage Example |\n|-------------|----------------------|---------------|\n| No placeholders | Static property | `Text(.introductionTitle)` |\n| With placeholders | Function with positional (unlabeled) arguments | `.youHaveFriends(42)` |\n\n**Key naming conversion**:\n- `App.HomeScreen.Title` → `.appHomeScreenTitle`\n- Periods removed, camel-cased\n- Available on `LocalizedStringResource`\n\nPlaceholder arguments are **positional**, not labeled, and take their types from the specifiers — `generate-symbols` writes:\n\n```swift\nstatic func youHaveFriends(_ arg1: Int) -> LocalizedStringResource\nstatic func sentMessages(_ arg1: String, _ arg2: Int) -> LocalizedStringResource\n```\n\nso `.youHaveFriends(42)` compiles and `.youHaveFriends(arg1: 42)` does not (`error: extraneous argument label 'arg1:' in call`). A catalog's `argumentNames` array does not change this — it is metadata, not a parameter name.\n\n#### Code Examples\n\n```swift\n// SwiftUI views\n@available(iOS 26, *) // navigationSubtitle(_:) is iOS 26+\nstruct ContentView: View {\n var body: some View {\n NavigationStack {\n Text(.introductionTitle)\n .navigationSubtitle(.subtitle(42))\n }\n }\n}\n\n// Foundation String\nlet message = String(localized: .curatedCollection)\n\n// Custom views with LocalizedStringResource\nstruct CollectionDetailEditingView: View {\n let title: LocalizedStringResource\n\n init(title: LocalizedStringResource) {\n self.title = title\n }\n\n var body: some View {\n Text(title)\n }\n}\n\nCollectionDetailEditingView(title: .editingTitle)\n```\n\n---\n\n### Automatic Comment Generation\n\nXcode 26 uses an **on-device model** to automatically generate contextual comments for localizable strings.\n\n#### Enabling the Feature\n\n1. Open Xcode Settings → Editing\n2. Enable \"automatically generate string catalog comments\"\n3. New strings added to code automatically receive generated comments\n\n#### Example\n\nFor a button string, Xcode generates:\n\n> \"The text label on a button to cancel the deletion of a collection\"\n\nThis context helps translators understand where and how the string is used.\n\n#### XLIFF Export\n\nAuto-generated comments are marked in exported XLIFF files:\n\n```xml\n\n Grand Canyon\n Grand Canyon\n Suggestion for searching landmarks\n\n```\n\n**Benefits**:\n- Saves developer time writing translator context\n- Provides consistent, clear descriptions\n- Improves translation quality\n\n---\n\n### Swift Package & Framework Localization\n\n#### The Problem\n\nSwiftUI uses the `.main` bundle by default. Swift Packages and frameworks need to reference their own bundle:\n\n```swift\n// ❌ Wrong - uses main bundle, strings not found\nText(\"My Collections\", comment: \"Section title\")\n```\n\n#### The Solution: #bundle Macro (NEW in Xcode 26)\n\nThe `#bundle` macro automatically references the correct bundle for the current target:\n\n```swift\n// ✅ Correct - automatically uses package/framework bundle\nText(\"My Collections\", bundle: #bundle, comment: \"Section title\")\n```\n\n**Key advantages**:\n- Works in main app, frameworks, and Swift Packages\n- Backwards-compatible with older OS versions\n- Eliminates manual `.module` bundle management\n\n#### With Custom Table Names\n\n```swift\n// Main app\nText(\"My Collections\",\n tableName: \"Discover\",\n comment: \"Section title\")\n\n// Framework or Swift Package\nText(\"My Collections\",\n tableName: \"Discover\",\n bundle: #bundle,\n comment: \"Section title\")\n```\n\n---\n\n### Custom Table Symbol Access\n\nWhen using multiple String Catalogs for organization:\n\n#### Default \"Localizable\" Table\n\nSymbols are directly accessible on `LocalizedStringResource`:\n\n```swift\nText(.welcomeMessage) // From Localizable.xcstrings\n```\n\n**Note**: Xcode automatically resolves symbols from the default \"Localizable\" table. Explicit table selection is rarely needed—use it only for debugging or testing specific catalogs.\n\n#### Custom Tables\n\nSymbols are nested in the table namespace:\n\n```swift\n// From Discover.xcstrings\nText(.Discover.featuredCollection)\n\n// From Settings.xcstrings\nText(.Settings.privacyPolicy)\n```\n\n**Organization strategy for large apps**:\n- **Localizable.xcstrings** - Core app strings\n- **FeatureName.xcstrings** - Feature-specific strings (e.g., Onboarding, Settings, Discover)\n- Benefits: Easier to manage, clearer ownership, better XLIFF organization\n\n---\n\n### Two Localization Workflows\n\nXcode 26 supports two complementary workflows:\n\n#### Workflow 1: String Extraction (Recommended for new projects)\n\n**Process**:\n1. Write strings directly in code\n2. Use SwiftUI views (`Text`, `Button`) and `String(localized:)`\n3. Xcode automatically extracts to String Catalog\n4. Leverage automatic comment generation\n\n**Pros**: Simple initial setup, immediate start\n\n**Cons**: Less control over string organization\n\n```swift\n// ✅ String extraction workflow\nText(\"Welcome to WWDC!\", comment: \"Main welcome message\")\n```\n\n#### Workflow 2: Generated Symbols (Recommended as complexity grows)\n\n**Process**:\n1. Manually add strings to String Catalog\n2. Reference via type-safe symbols\n3. Organize into custom tables\n\n**Pros**: Better control, type safety, easier to maintain across frameworks\n\n**Cons**: Requires planning string catalog structure upfront\n\n```swift\n// ✅ Generated symbols workflow\nText(.welcomeMessage)\n```\n\n| Workflow | Best For | Trade-offs |\n|----------|----------|------------|\n| String Extraction | New projects, simple apps, prototyping | Automatic extraction, less control over organization |\n| Generated Symbols | Large apps, frameworks, multiple teams | Type safety, better organization, requires upfront planning |\n\n---\n\n### Refactoring Between Workflows\n\nXcode 26 allows converting between workflows without manual rewriting.\n\n#### Converting Strings to Symbols\n\n1. **Right-click** on a string literal in code\n2. Select **\"Refactor > Convert Strings to Symbols\"**\n3. **Preview** all affected locations\n4. **Customize** symbol names before confirming\n5. **Apply** to entire table or individual strings\n\n**Example**:\n\n```swift\n// Before\nText(\"Welcome to WWDC!\", comment: \"Main welcome message\")\n\n// After refactoring\nText(.welcomeToWWDC)\n```\n\n**Benefits**:\n- Batch conversion of entire String Catalogs\n- Preview changes before applying\n- Maintain localization without code rewrites\n\n---\n\n### Implementation Checklist\n\nAfter adopting Xcode 26 generated symbols, verify:\n\n**Build Configuration:**\n- [ ] \"Generate String Catalog Symbols\" build setting enabled\n- [ ] Project builds without \"Cannot find 'symbolName' in scope\" errors\n- [ ] Clean build succeeds (Cmd+Shift+K, then Cmd+B)\n\n**String Catalog Setup:**\n- [ ] Strings manually added to catalog using + button (not auto-extracted)\n- [ ] Symbol names follow conventions (camelCase, no periods)\n- [ ] Custom tables organized by feature (if using multiple catalogs)\n\n**Swift Package Integration:**\n- [ ] All `Text()` and `String(localized:)` calls in packages use `bundle: #bundle`\n- [ ] Import Foundation added where `#bundle` is used\n- [ ] Tested package builds independently and as dependency\n\n**Refactoring & Migration:**\n- [ ] Tested refactoring tool on sample strings\n- [ ] Preview showed expected changes before applying\n- [ ] Old string-based calls still work during transition period\n\n**Optional Features:**\n- [ ] Automatic comment generation enabled in Xcode Settings → Editing (optional)\n- [ ] Tested AI-generated comments for accuracy\n- [ ] XLIFF export includes auto-generated comments\n\n**Testing:**\n- [ ] Symbols resolve correctly in SwiftUI previews\n- [ ] Localization works across all supported languages\n- [ ] App runs on minimum supported iOS version\n\n---\n\n## Resources\n\n**WWDC**: 2025-225, 2023-10155, 2022-10110\n\n**Docs**: /xcode/localization, /xcode/localizing-and-varying-text-with-a-string-catalog\n\n**Skills**: skills/localization-research-ref.md (Apple terminology matching, glossary, VoiceOver comments, pseudolocalization, TMS), skills/app-intents-ref.md, axiom-design (skills/hig.md), axiom-accessibility\n", "skillType": "discipline", "source": "axiom", "tags": [ @@ -18131,91 +18131,91 @@ "heading": "Part 1: String Catalogs (WWDC 2023/10155)", "level": 2, "startLine": 32, - "endLine": 111, - "charCount": 2248 + "endLine": 162, + "charCount": 6515 }, { "heading": "Part 2: SwiftUI Localization", "level": 2, - "startLine": 112, - "endLine": 194, + "startLine": 163, + "endLine": 245, "charCount": 2125 }, { "heading": "Part 3: UIKit & Foundation", "level": 2, - "startLine": 195, - "endLine": 248, + "startLine": 246, + "endLine": 299, "charCount": 1325 }, { "heading": "Part 4: Pluralization", "level": 2, - "startLine": 249, - "endLine": 382, + "startLine": 300, + "endLine": 433, "charCount": 4915 }, { "heading": "Part 5: Device & Width Variations", "level": 2, - "startLine": 383, - "endLine": 474, + "startLine": 434, + "endLine": 525, "charCount": 2652 }, { "heading": "Part 6: RTL Support", "level": 2, - "startLine": 475, - "endLine": 544, + "startLine": 526, + "endLine": 595, "charCount": 1405 }, { "heading": "Part 7: Locale-Aware Formatting", "level": 2, - "startLine": 545, - "endLine": 619, + "startLine": 596, + "endLine": 670, "charCount": 1710 }, { "heading": "Part 8: App Shortcuts Localization", "level": 2, - "startLine": 620, - "endLine": 672, + "startLine": 671, + "endLine": 723, "charCount": 1226 }, { "heading": "Part 9: Migration from Legacy", "level": 2, - "startLine": 673, - "endLine": 715, + "startLine": 724, + "endLine": 766, "charCount": 1645 }, { "heading": "Common Mistakes", "level": 2, - "startLine": 716, - "endLine": 799, - "charCount": 1856 + "startLine": 767, + "endLine": 872, + "charCount": 3024 }, { "heading": "Troubleshooting", "level": 2, - "startLine": 800, - "endLine": 880, + "startLine": 873, + "endLine": 953, "charCount": 3009 }, { "heading": "Part 10: Xcode 26 Localization Enhancements", "level": 2, - "startLine": 881, - "endLine": 1183, + "startLine": 954, + "endLine": 1256, "charCount": 8735 }, { "heading": "Resources", "level": 2, - "startLine": 1184, - "endLine": 1191, + "startLine": 1257, + "endLine": 1264, "charCount": 357 } ], @@ -38110,7 +38110,7 @@ "axiom-swiftui--iphone-duo": { "name": "axiom-swiftui--iphone-duo", "description": "iPhone Duo is Apple's first two-display iPhone: an outer display, and a larger inner display that folds along a hinge. This skill covers adapting an app to it — the device model, what changes with the SDK you build against, vertical bars, the fold, and the new layout, hinge, and scene tools. SwiftUI comes first; the UIKit equivalent sits beside it.", - "content": "# iPhone Duo\n\niPhone Duo is Apple's first two-display iPhone: an outer display, and a larger inner display that folds along a hinge. This skill covers adapting an app to it — the device model, what changes with the SDK you build against, vertical bars, the fold, and the new layout, hinge, and scene tools. SwiftUI comes first; the UIKit equivalent sits beside it.\n\n## SDK Status — Read First\n\nApple announced the Duo-specific APIs in six tech talks (111461–111466, September 2026). They shipped in the **iOS 27.1 SDK** (and are in the 27.2 SDK); every snippet below is compiled against it. Gate 27.1 calls at `@available(iOS 27.1, *)` and keep the pre-27.1 path compiling.\n\n- **Check the installed SDK first** (`xcrun --sdk iphoneos --show-sdk-version`). Below 27.1 the 27.1 symbols don't exist — describe the API, name the talk, and give today's alternative. On 27.1 or later: grep the SDK's `.swiftinterface` and headers for the name; the SDK's spelling and signature win over this file; if it's missing, check for a rename before calling it dropped.\n- **Never call iPhone Duo or these APIs fictional or hallucinated.** They come from Apple's own tech talks and are in the shipped SDK.\n- **Never invent parameters, types, or cases** beyond what the snippets and the SDK give.\n\n## When to Use This Skill\n\nUse when:\n- Preparing an app for iPhone Duo, a foldable iPhone, or a two-display iPhone\n- Layout breaks in some poses — closed, open, rotated, or partially folded\n- Toolbar, tab bar, or navigation bar items should move to the side of the screen\n- Interactive UI lands in the fold or under the inner camera\n- Choosing between reserved regions, arrangements, and the hinge\n- Showing content on another display, or opening multiple windows on iPhone\n- Code or a question names a 27.1 API (`onHingeChange`, `ArrangementView`, `reservedRegions`, `axisBehavior`, …)\n\n#### Related Skills\n- axiom-uikit (skills/uikit-modernization.md) — the resizing baseline Duo builds on: scene lifecycle, geometry, size classes\n- skills/layout.md — adaptive layout; the size-class truth tables include Duo\n- skills/toolbars.md — SwiftUI placements, overflow, and visibility priority\n- axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md) — Duo front cameras and camera direction\n\n## Example Prompts\n\n#### 1. \"How do I prepare my app for iPhone Duo?\"\n→ Readiness Today: resizing baseline, per-side safe areas, standard containers; build with the 27.1 SDK or later.\n\n#### 2. \"How do I detect that my app is running on iPhone Duo?\"\n→ Don't. It's still an iPhone app — use size classes and scene geometry.\n\n#### 3. \"My toolbar buttons should move to the side on iPhone Duo.\"\n→ Vertical Bars: system-managed bars only, titles on every item, overflow priorities.\n\n#### 4. \"A button sits in the fold when the phone is partly closed.\"\n→ The Fold and the Camera: displacement rules.\n\n#### 5. \"Can I use the hinge angle in my app?\"\n→ Hinge: effects and interactions, never layout.\n\n#### 6. \"Add .axisBehavior(.horizontalOnly) to my Select button\"\n→ Vertical Bars: the axis APIs are iOS 27.1; a build against 27.0 or earlier never gets vertical bars — ship titles, images, and priorities until you rebuild.\n\n## Red Flags — Anti-Patterns to Prevent\n\n| Thought | Reality |\n|---|---|\n| \"I'll check the model identifier and give Duo its own layout\" | It's still an iPhone app — compact width outside, regular width inside. Model and idiom checks break in Split View, in iPhone Mirroring, and on the next device. Use size classes and scene geometry. |\n| \"I'll branch on interface orientation\" | The inner display ignores your supported orientations. Decide layout with size classes. |\n| \"`UIScreen.main` gives me the screen\" | Deprecated since iOS 26, and ambiguous with two displays. Use `window?.windowScene?.screen` or `traitCollection.displayScale`. |\n| \"Safe-area insets are symmetric\" | A vertical bar sits on one side, so left ≠ right — and in Split View it switches sides. Inset each side independently. |\n| \"I'll build my own bottom bar\" | A custom `UIToolbar`/`UINavigationBar`/`UITabBar` or a hand-built SwiftUI row never moves to the side, and a hand-built row gets none of the system's fold avoidance. Use system-managed bars. |\n| \"I'll hide the controls when it's folded\" | Displace, never hide: move, resize, or reorganize so every function stays reachable in every pose. |\n| \"I'll read the hinge angle to size my panes\" | The hinge drives effects and interactions. Layout uses arrangements and reserved regions. |\n| \"Each pose gets its own layout\" | Design for the two horizontal size classes — compact outside, regular inside. An optional tabletop layout must keep every control and the same hierarchy. |\n| \"It's on the inner display, so it's wide\" | A Split View half reports compact width, like the outer display (measured on the 27.1 Duo simulator). Read the size class from the environment; never key a wide layout to the display. |\n| \"The New Window button can always show\" | The outer display can't create windows. Gate the affordance. |\n\n## The Device\n\n| Display | Horizontal | Vertical |\n|---|---|---|\n| Outer, portrait | `.compact` | `.regular` |\n| Outer, landscape | `.compact` | `.compact` |\n| Inner, full screen | `.regular` | `.regular` |\n| Inner, one half of Split View | `.compact` | `.regular` |\n\n- **Poses** Closed; open in portrait or landscape; partially folded like a book; seated like a laptop (tabletop) with the inner display facing you; standing on its edges.\n- **Still an iPhone app** Adapt to size classes and scene bounds, never to the device.\n- **Controls on the side** Built against the 27.1 SDK, in every pose except inner-display portrait, bars lay out vertically along the side, sharing that edge with the status bar, the Dynamic Island, and Live Activities. When space runs out, items collapse into the overflow menu.\n- **Multitasking** A 50/50 split view places two apps side by side, each with its controls on its outer edge (`toolbarVerticalEdge` reads `.leading` left, `.trailing` right) and the compact-width layout in each half. Picture in Picture can pin to the top; the app below resizes vertically.\n- **Offset, don't center** Most content offsets away from the side controls — align to horizontal safe-area insets and it happens for you. Center on the full display only for non-scrolling, highly visual UI whose interactive elements the controls can't cover. A full-width background under inset scrolling content also works.\n- **Inner display** Don't stretch the iPhone layout. Use a split view, a two-column rearrangement when width allows, or a tab sidebar for information-dense apps. Keep the hierarchy identical inside and out — people open and close the device mid-task.\n- **Games** Lock to portrait or landscape, but fill the screen in every pose; change the aspect ratio rather than letterboxing or pillarboxing (HIG).\n\n## Behavior by the SDK You Build Against\n\nLink-time behavior — what the device does with your binary:\n\n| Built against | On iPhone Duo |\n|---|---|\n| Pre-27 SDK | Closed: runs in the space beside the status bar and camera. Open: a familiar size and aspect ratio |\n| iOS 27.0 SDK | Resizes like any 27 iPhone app; extends left of the status bar on the inner display |\n| iOS 27.1 SDK | Edge to edge; standard navigation, toolbar, and tab bars lay out vertically |\n\n`UIRequiresFullScreen` is still honored, but the app still resizes when the device opens or closes. Supported orientations govern the outer display as on any iPhone; the inner display doesn't honor them — the app scales there instead, including in Split View.\n\n## Which Tool When\n\n| Need | Tool | Availability |\n|---|---|---|\n| Navigation, tabs, sheets, alerts, and menus that adapt to every pose | Standard containers (`NavigationSplitView`, `TabView`, `UISplitViewController`, …) | Today |\n| Custom UI that must avoid the fold or the inner camera | Reserved regions | 27.1 |\n| Two views that split side by side or overlay | Arrangements | 27.1 |\n| An effect or interaction driven by the fold angle | Hinge | 27.1 |\n| Extra content on another display | Scene accessories | Today for external displays; Duo camera variant 27.1 |\n| A second window of your app | Multiple scenes, inner display only | Today |\n\n## Readiness Today\n\nThe resizing baseline is the same as for every 27 iPhone app — scene lifecycle, no `UIScreen.main`, size classes over orientation and idiom: axiom-uikit (skills/uikit-modernization.md). Adaptive patterns: skills/layout.md. Duo adds five things.\n\n#### Handle each safe-area side independently\n\n```swift\n// Wrong on Duo: a vertical bar on one side makes left != right\nlet assumedWidth = view.bounds.width - view.safeAreaInsets.left * 2\n// Right: inset each side on its own\nlet contentWidth = view.bounds.inset(by: view.safeAreaInsets).width\n```\n\nLayout margins are asymmetric too. SwiftUI places content inside the safe area by default; let backgrounds extend with `.ignoresSafeArea()`, and in UIKit size backgrounds to `view.bounds`. With Auto Layout, constrain to `safeAreaLayoutGuide` / `layoutMarginsGuide` — each side stays independent (axiom-uikit (skills/adaptive-layout.md)). Under right-to-left languages the bar stays on the hardware side, so never assume the trailing inset is the larger one.\n\n#### Offer a tab sidebar on the inner display `iOS27`\n\n```swift\nTabView {\n Tab(\"Summary\", systemImage: \"heart\") { SummaryView() }\n Tab(\"Browse\", systemImage: \"square.grid.2x2\") { BrowseView() }\n}\n.tabViewStyle(.sidebarAdaptable) // required: the placement applies only to this style\n.defaultTabBarPlacement(.sidebar)\n```\n\nUIKit: `tabBarController.sidebar.preferredPlacement = .sidebar`. The SwiftUI modifier has no effect on iPadOS, where the bar adapts on its own — use `defaultAdaptableTabBarPlacement(_:)` there. A sidebar suits information-dense apps; most apps keep the tab bar.\n\n#### Gate new-window affordances\n\niPhone Duo is the first iPhone with multiple windows of one app, and only its inner display can create them. Both paths need `UIApplicationSupportsMultipleScenes` set to `YES` in the scene manifest (axiom-uikit (skills/uikit-modernization.md)), and `openWindow(id:value:)` needs a matching `WindowGroup(id:for:)` (axiom-design (skills/app-composition.md)). Apple's doc comment for `supportsMultipleWindows` limits `true` to macOS and iPadOS with that key — but on iPhone Duo the key is what decides: measured on the 27.1 Duo simulator, the value is `true` on the closed outer display and the open inner display alike, and it does not change as the device opens or closes; without the key it is `false`. So it reports that the manifest is configured, not which display can create windows — rely on the system's own control (`UIWindowScene.ActivationAction`, which hides itself where new windows aren't available) rather than gating on this value alone.\n\n```swift\nstruct ItemRow: View {\n let item: Item\n @Environment(\\.supportsMultipleWindows) private var supportsMultipleWindows\n @Environment(\\.openWindow) private var openWindow\n\n var body: some View {\n Text(item.title)\n .contextMenu {\n if supportsMultipleWindows {\n // true on both Duo displays; the request still fails on the outer one\n Button(\"Open in New Window\") { openWindow(id: \"detail\", value: item.id) }\n }\n }\n }\n}\n```\n\n```swift\n// UIKit menu action (iOS 15): where new windows aren't available the alternate runs;\n// with no alternate, the talks say the item hides (111464)\nlet openHere = UIAction(title: \"Open\") { _ in showDetailInCurrentWindow() }\nlet newWindow = UIWindowScene.ActivationAction(alternate: openHere) { _ in\n UIWindowScene.ActivationConfiguration(userActivity: detailActivity)\n}\n\n// Direct request: fails on the outer display, so handle the error (iOS 17)\nlet request = UISceneSessionActivationRequest(role: .windowApplication, userActivity: detailActivity)\nUIApplication.shared.activateSceneSession(for: request) { error in\n logger.error(\"Window request failed: \\(error.localizedDescription, privacy: .public)\")\n showDetailInCurrentWindow()\n}\n```\n\nThe Swift name is `UIWindowScene.ActivationAction`; the ObjC name `UIWindowSceneActivationAction` doesn't compile in Swift. The value is pose-independent (measured on the 27.1 Duo simulator), so the affordance doesn't need rebuilding as the device folds — and where new windows aren't available, `UIWindowScene.ActivationAction` hides itself.\n\n#### Match the new corners and support landscape\n\n`ConcentricRectangle` / `UICornerConfiguration` (iOS 26) are updated for Duo's display corners — skills/26-ref.md (Corner Concentricity). Support landscape on the outer display; people may set the phone down like a tent.\n\n#### Test both halves of Split View\n\nDrag the app to the left half, then the right. The vertical bar follows the app's outer edge, so the larger safe-area inset switches sides — `toolbarVerticalEdge` reads `.leading` in the left half and `.trailing` in the right (measured on the 27.1 Duo simulator). A half reports **compact width / regular height**, the same width class as the outer display, so the compact layout is what people see in Split View; read it from the environment rather than keying a wide layout to the display.\n\n## Vertical Bars\n\nBuilt against the 27.1 SDK, navigation, toolbar, and tab bar items share one vertical stack along the side — picture the horizontal bars rotated 90°. Only **system-managed bars** take part: `NavigationStack`, `NavigationSplitView`, or `TabView` with `.toolbar`, or `UINavigationController` and `UITabBarController` with items set on the view controller. The content of a custom `UIToolbar`, `UINavigationBar`, or `UITabBar` never joins the vertical bar; it stays where you put it.\n\n#### Which bars go vertical\n\n- Only a split view's detail column; other columns keep horizontal bars. Inspectors get no vertical bar.\n- Sheets: on the outer display a sheet's toolbar goes vertical by default (disable it with the off switch below); on the inner display sheets center with horizontal bars. A sheet placed on the right gets a vertical bar; one on the left doesn't. The placement APIs — `.presentationPlacement(.trailing)` and `sheetPresentationController?.preferredPlacement = .trailing`, both iOS 27; the UIKit one is ignored when `sourceView` is set — take leading/trailing, but the rule is physical right/left.\n- Controls that belong to a content area stay with it, not on the side — Mail keeps the list's controls above the leading pane (HIG).\n- The bar stays on the hardware side under right-to-left languages; content adapts around it.\n- Keyboard accessory bars stay on the keyboard.\n- The inner display in portrait keeps horizontal bars.\n\n#### Order items top to bottom\n\n1. Back (automatic in a navigation container) or a custom close. SwiftUI: `.cancellationAction`. UIKit: a leading item, with `leftItemsSupplementBackButton` left `false` (the default).\n2. The prominent action. SwiftUI: `.topBarPinnedTrailing` `iOS27`. UIKit: `pinnedTrailingGroup`.\n3. Everything else, in its existing groups. A spacer separates top and bottom placements.\n\nTop-bar items go to the top of the stack, bottom-bar items to the bottom, and the tab bar stays bottom-aligned.\n\nKeep placement consistent across poses so people don't relearn where actions live. SwiftUI code for these placements and for overflow: skills/toolbars.md (Pattern 2, Pattern 11). UIKit:\n\n```swift\nnavigationItem.leftItemsSupplementBackButton = false // the custom close replaces back\nnavigationItem.leadingItemGroups = [UIBarButtonItemGroup(barButtonItems: [closeItem], representativeItem: nil)]\nnavigationItem.pinnedTrailingGroup = UIBarButtonItemGroup(barButtonItems: [doneItem], representativeItem: nil)\nnavigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { completion in\n completion([UIAction(title: \"Scan\", image: UIImage(systemName: \"doc.viewfinder\")) { _ in scan() }])\n}\nshareItem.visibilityPriority = .high // iOS27: collapses after standard-priority items\ninboxItem.badge = .count(7) // iOS 26: a symbol-only item that still shows the count\n```\n\n`UIBarButtonItem.Badge` is itself main-actor isolated — even `.count(n)` can't be built in a nonisolated model.\n\n#### Make items vertical-ready\n\n- Give every item a title and an image — a SwiftUI `Label`, or a `UIBarButtonItem` with both. Bars show the icon; the overflow menu shows title and icon.\n- Items with an icon go vertical; text-only items stay horizontal. An item that switches between a symbol and text (a custom Select/Done) belongs on the horizontal axis — the system edit button already stays there.\n- Replace inline counts with a badge. Text that carries real information, like a cart total, stays in a horizontal bar.\n- Custom views, complex views, and wide controls like segmented controls stay horizontal unless opted in (Axis, edge, compression, and the off switch).\n- Vertical bars have a fixed width and flexible height; once a custom view opts in, it must fit that width or adapt its layout. Flexible spacers collapse to zero vertically; fixed spacers keep their minimum. Don't add extra spacing — group items with `ToolbarItemGroup` / `UIBarButtonItemGroup`, which supply it and adapt it.\n- Vertical bars have no scroll-edge effect but gain a background under Reduce Transparency — keep custom content legible either way.\n- A hero or background image extends under the vertical bar with `.backgroundExtensionEffect()` (SwiftUI) or `UIBackgroundExtensionView` (UIKit).\n\n#### Plan for overflow\n\nThe outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up. A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar.\n\n#### When to turn vertical bars off\n\nA single-page, bottom-heavy layout like a calculator, or a sheet whose only item is Close, may work better with horizontal bars.\n\n#### Axis, edge, compression, and the off switch\n\nThe inferred axis is usually right — a title-only item stays horizontal, an item with an image goes vertical. Override it per item when a custom view, a wide control, or a symbol↔text toggle needs a specific axis. All of these are iOS 27.1; below that, items keep whatever axis the system infers, and the knobs don't exist. A build against the 27.0 SDK or earlier never sees a vertical bar at all.\n\n```swift\n// SwiftUI — iOS 27.1: per-item axis override, compression order, and the edge read\n@available(iOS 27.1, *)\nstruct BarControls: View {\n @Environment(\\.toolbarVerticalEdge) private var edge: HorizontalEdge?\n\n var body: some View {\n NavigationStack {\n SummaryView()\n .toolbar {\n ToolbarItem {\n Button(\"Select\", systemImage: \"checkmark.circle\") { }\n }\n .axisBehavior(.horizontalOnly) // symbol↔text toggles and wide controls stay horizontal\n ToolbarItem {\n Button(\"Compass\", systemImage: \"location.north.circle\") { }\n }\n .axisBehavior(.verticalPreferred) // opt a custom view in (icon items infer this already)\n }\n .toolbarVerticalCompressionBehavior(.prefersToolbarItems) // tab bar compresses first\n .overlay(alignment: .bottom) {\n // .leading / .trailing while a bar is vertical; nil when items can't go vertical\n Text(edge == .trailing ? \"Bar: trailing\" : edge == .leading ? \"Bar: leading\" : \"No vertical bar\")\n .font(.caption)\n }\n }\n }\n}\n\n@available(iOS 27.1, *)\nstruct HorizontalBarsOnly: View {\n var body: some View {\n SummaryView()\n .toolbarVerticalBehavior(.disabled) // bottom-heavy screens keep horizontal bars\n }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the item's axis, the compression preference, the off switch, the edge\n@available(iOS 27.1, *)\nfinal class PlayerViewController: UIViewController {\n override var preferredVerticalBarBehavior: UIVerticalBarBehavior { .disabled }\n}\n\n@MainActor\nfunc tuneVerticalBar(_ item: UIBarButtonItem, _ navigationItem: UINavigationItem, _ traits: UITraitCollection) {\n guard #available(iOS 27.1, *) else { return } // below 27.1 the knobs don't exist; bars stay horizontal\n item.axisBehavior = .verticalPreferred // .horizontalOnly keeps a wide custom view put\n navigationItem.verticalBarCompressionBehavior = .prefersBarItems // keep the item; compress the tab bar\n if traits.verticalBarEdge == .trailing {\n // .leading / .trailing / .unspecified — inset the side that holds the bar\n }\n}\n```\n\n## The Fold and the Camera\n\nWhen iPhone Duo is partially folded, the display curves through the center and splits into regions. Two kinds of **reserved region** shape the usable space:\n\n- **Division** — the fold. Active only while partially folded; zero width when flat. The element is a `ReservedRegion` (SwiftUI) or `UIView.ReservedRegion` (UIKit); both expose `frame`, `margins`, and `isActive`.\n- **Occlusion** — the inner FaceTime camera. Active only while that camera runs.\n\nThe outer display's camera is always present — it shares the corner with the Dynamic Island, which expands for Live Activities — and the system accounts for it, including when bars go to the side (HIG).\n\n#### Displacement rules\n\n- Move, resize, or reorganize — never hide. Every function stays reachable in every pose.\n- Move elements that work together as a unit; move independent elements alone.\n- Avoid long moves; distance weakens the link between an element and its source.\n- Scrolling content — articles, feeds, lists — never displaces; it already adapts by scrolling.\n- Let purpose choose the destination. Book pose: alerts move to the trailing side, where they'll be when the device closes. Tabletop: content meant to be seen from a distance goes to the top region; tappable controls go to the bottom, a stable surface.\n- Stay contextual: search stays over the view it searches.\n\nSystem components already avoid the fold: sheets, alerts, action sheets, menus, popovers, and toolbar buttons, and split views rebalance to an even 50/50. Use them wherever you can.\n\n#### Custom grids\n\nThese change spacing and column count, not which region content lives in, so they aren't displacement.\n\n- **Keep each item inside one region while folded.** Preserve the outer margins and widen the spacing around the fold (Apple's Fitness example, 111463 5:56).\n- **Consider an even column count.** Apple suggests preferring an even number of columns when a division region exists, active or not (111463 7:36). The middle gap lands on the fold only when the grid is centered on the display and the fold runs vertically through it, as in book pose; otherwise place the gap from the region's `frame`. `GridItem(.adaptive(minimum:))` picks its own count, which can be odd.\n- **Find the fold.** Read the division region's `frame`, passing `.includeInactive` for the column decision. Below 27.1 only system components know where the fold is: don't hard-code the display's midpoint or check the device model.\n\n```swift\n// SwiftUI — iOS 27.1: the fold's and the camera's regions, in view coordinates\n@available(iOS 27.1, *)\nfunc foldGap(proxy: GeometryProxy) -> CGFloat {\n proxy.reservedRegions(kind: .division, options: .includeInactive)\n .filter(\\.isActive)\n .map(\\.frame.width)\n .max() ?? 0\n}\n\n// UIKit — iOS 27.1\n@MainActor\nfunc cameraOcclusionRect(in view: UIView) -> CGRect? {\n guard #available(iOS 27.1, *) else { return nil }\n return view.reservedRegions(kind: .occlusion).first?.frame\n}\n```\n\n## Arrangements\n\nAn arrangement places a primary and a secondary view by rules — size classes, aspect ratio, and active fold regions. It sits between navigation containers and content containers.\n\n- **Split** — main and detail content where neither view may be obscured, like a player and its transcript. The default style; it splits along the longer axis unless restricted. If the split can't use the view's long axis (e.g. `.axes(.horizontal)` in a tall view), it shows a single view (the primary, in the talk's example) — keep the secondary reachable another way.\n- **Overlay** — a clear foreground and background, like controls over readable content. Closed or fully open, it layers the primary over the secondary; partially folded, the primary moves to the trailing (or bottom) region and the secondary to the leading (or top).\n- Follow existing patterns: an HStack or VStack split becomes a split arrangement; a ZStack overlay becomes an overlay arrangement.\n- Never nest a navigation container inside an arrangement, and never put an arrangement inside a `List` or `ScrollView`.\n- Put it inside the navigation container — the talks nest it in a `NavigationStack` and make the UIKit controller the navigation root. Use it for split-like layout without a split view's expand/collapse.\n\nThe API is iOS 27.1; below that, use `NavigationSplitView` (or nested stacks) for the same jobs — arrangements don't exist there.\n\n```swift\n// SwiftUI — iOS 27.1: split or overlay by rules\n@available(iOS 27.1, *)\nstruct PlayerScreen: View {\n var body: some View {\n NavigationStack {\n ArrangementView {\n PlayerView()\n } secondary: {\n UpNextView()\n }\n .arrangementViewStyle(.split.axes(.horizontal)) // or .overlay\n }\n }\n}\n\n@available(iOS 27.1, *)\nstruct PlayerView: View {\n var body: some View { Text(\"Now playing\") }\n}\n\n@available(iOS 27.1, *)\nstruct UpNextView: View {\n @Environment(\\.overlayArrangementZIndex) private var zIndex: Int // changes as the device folds\n var body: some View { Text(zIndex > 0 ? \"Collapsed\" : \"Expanded\") }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the arrangement controller is the navigation root\n@available(iOS 27.1, *)\n@MainActor\nfinal class ArrangementHost {\n let controller = UIArrangementViewController()\n\n func place(player: UIViewController, upNext: UIViewController) {\n controller.setViewController(player, for: .primary)\n controller.setViewController(upNext, for: .secondary)\n controller.updateArrangement(.split.axes(.horizontal))\n }\n\n func primaryZIndex() -> Int {\n controller.state(for: .primary)?.zIndex ?? 0\n }\n}\n```\n\n## Hinge\n\nThe hinge reports a status — closed, partially open, fully open — and a continuous angle. Use it for effects and interactions, like a pitch bend or a zoom that follows the fold, never for layout. A missing hinge means the device has none; reset hinge-driven state whenever the device isn't partially open. The API is iOS 27.1; below that your app never sees hinge updates.\n\n```swift\n// SwiftUI — iOS 27.1\n@available(iOS 27.1, *)\nstruct HingeDrivenView: View {\n @State private var bend = Angle.zero\n\n var body: some View {\n Text(\"Pitch bend\")\n .rotationEffect(bend)\n .onHingeChange { _, new in\n if let hinge = new.hinge, hinge.status == .partiallyOpen {\n bend = hinge.angle\n } else {\n bend = .zero // no hinge, or not partially open: reset\n }\n }\n }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the same state, delivered to an interaction\n@available(iOS 27.1, *)\n@MainActor\nfinal class HingeObserver {\n private var interaction: UIHingeInteraction?\n\n func attach(to view: UIView, onChange: @escaping (UIHinge.Status, CGFloat) -> Void) {\n let interaction = UIHingeInteraction { _, update in\n guard let hinge = update.hinge else { return } // nil: this hierarchy provides no hinge\n onChange(hinge.status, hinge.angle) // angle is in radians\n }\n view.addInteraction(interaction)\n self.interaction = interaction\n }\n}\n```\n\n## Scenes and Accessories\n\nA **scene accessory** is supplementary content the system presents for you when a capability becomes available. The system decides when and where it appears, and your app must stay fully functional without it.\n\n#### External-display accessory `iOS27`\n\nToday's accessory targets an external display, connected or over AirPlay:\n\n```swift\nstruct PresenterView: View {\n let deck: Deck\n @State private var showsAudienceView = true\n @State private var accessoryAvailable = false\n\n var body: some View {\n SlideEditor(deck: deck)\n .toolbar {\n ToolbarItem {\n Toggle(\"Audience View\", systemImage: \"rectangle.on.rectangle\", isOn: $showsAudienceView)\n .disabled(!accessoryAvailable)\n }\n }\n .sceneAccessory {\n ExternalNonInteractiveAccessory(isEnabled: $showsAudienceView) {\n AudienceSlide(deck: deck)\n }\n .onAvailabilityChange { accessoryAvailable = $0 }\n }\n }\n}\n```\n\nRegister the accessory on the view whose visibility should gate it.\n\n#### The Duo camera accessory\n\nOn iPhone Duo, a camera variant shows UI on the outer display — a teleprompter, or something to show the person being photographed — while your camera UI runs on the inner display. It's available only while the app is full screen on the inner display with an active camera session, and it's iOS 27.1 — below that the type doesn't exist, so ship without it. Camera direction and the new front cameras: axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md).\n\n```swift\n// SwiftUI — iOS 27.1: outer-display UI while an inner-display camera session runs\n@available(iOS 27.1, *)\nstruct CameraScreen: View {\n @State private var showsOuterUI = true\n @State private var accessoryAvailable = false\n\n var body: some View {\n Text(\"Camera preview\")\n .sceneAccessory {\n CameraCaptureAccessory(isEnabled: $showsOuterUI) {\n Text(\"Teleprompter\")\n }\n .onAvailabilityChange { accessoryAvailable = $0 }\n }\n }\n}\n```\n\nUIKit registers the same content on the capture view controller and holds the returned registration:\n\n```swift\n// UIKit — iOS 27.1\n@available(iOS 27.1, *)\n@MainActor\nfinal class ScriptSceneDelegate: NSObject, UIWindowSceneDelegate {\n func scene(_ scene: UIScene, willConnectTo session: UISceneSession,\n options connectionOptions: UIScene.ConnectionOptions) {\n // connectionOptions.sceneAccessoryUserInfo carries the object you pass as userInfo, when you pass one\n }\n}\n\n@available(iOS 27.1, *)\n@MainActor\nfinal class CameraViewController: UIViewController {\n private var registration: UISceneAccessoryRegistration?\n\n override func viewDidLoad() {\n super.viewDidLoad()\n let configuration = UISceneConfiguration()\n configuration.delegateClass = ScriptSceneDelegate.self\n registration = registerSceneAccessory(\n .cameraCapture(sceneConfiguration: configuration)\n )\n }\n}\n```\n\nAvailability (`isAvailable`) and your on/off switch (`isEnabled`) are separate: the system decides when it can present, you decide whether to offer. Content appears only while the app is foreground, capturing, and on the inner display; it yields to the top-most registration of its kind, and goes away when capture stops, the app backgrounds, or the device closes — keep essential controls on the inner display and treat the accessory as an enhancement. Test on hardware; Simulator has no camera. Apple article: \"Registering a camera capture accessory on iPhone Duo\".\n\n## Tooling and Testing\n\n- **Device Hub** — Xcode 27.1's Device Hub drives an iPhone Duo simulator with open, close, rotate, and fold controls (111461 0:56); Apple's overview notes the Duo simulator in Device Hub requires Xcode 27.1. The device type creates against the **iOS 27.1 runtime**; the 27.0 runtime rejects it (`Incompatible device`). `iPhone Fold` is a different product.\n- **Simulator gaps** — per the Xcode 27.1 beta release notes, StandBy is unavailable in the iPhone Duo Simulator runtime, and running and debugging most app extensions is unavailable there.\n- **App Resizability** — Xcode's app-modernization agent skill, renamed \"App Resizability\", now covers SwiftUI and iPhone Duo (111461 9:15). See axiom-uikit (skills/uikit-modernization.md).\n\n## Pressure Scenarios\n\n#### \"Ship Duo support by Friday — just check for the Duo model\"\nA model check covers one device and breaks in Split View and iPhone Mirroring. Size classes and per-side safe areas take the same time and cover every pose. Push back: \"Size classes handle Duo and every future device; a model check handles one.\"\n\n#### \"That API doesn't exist — drop the Duo section\"\nThe APIs come from Apple's September 2026 tech talks and shipped in the iOS 27.1 SDK. Keep the guidance, verify each name against the installed SDK, and where the SDK you build with is older, ship today's alternatives and say what the 27.1 rebuild brings (the bar behavior is link-time).\n\n#### \"Just hide the controls when it's folded\"\nHiding ties functionality to a pose. Move the controls to the region that suits their purpose; system components already do this.\n\n## Checklist\n\n- ☑ No model, idiom, or orientation checks drive layout\n- ☑ No `UIScreen.main`; geometry comes from the scene or the view\n- ☑ Safe-area and margin math handles each side independently\n- ☑ Corner configuration matches Duo's display corners; the outer display supports landscape\n- ☑ Bars are system-managed; every item has a title and an image\n- ☑ Items run back/close → prominent → the rest; overflow priorities are set\n- ☑ New-window affordances are gated; scene-request errors are handled\n- ☑ Interactive UI stays out of the fold through system components or displacement — never by hiding\n- ☑ Tested closed, open in both orientations, partially folded, and in both halves of Split View\n- ☑ 27.1 APIs carry `@available(iOS 27.1, *)`; the app still compiles against the older SDK you support\n\n## Resources\n\n**Tech Talks**: 111461, 111462, 111463, 111464, 111465, 111466\n\n**Docs**: /swiftui/view/defaulttabbarplacement(_:), /swiftui/view/sceneaccessory(content:), /swiftui/externalnoninteractiveaccessory, /swiftui/environmentvalues/supportsmultiplewindows, /uikit/uiwindowscene/activationaction, /uikit/uiapplication/activatescenesession(for:errorhandler:), /uikit/uinavigationitem/pinnedtrailinggroup, /technologyoverviews/preparing-your-app-for-iphone-duo, /design/human-interface-guidelines/designing-for-iphone-duo\n\n**Skills**: axiom-uikit (skills/uikit-modernization.md), skills/layout.md, skills/toolbars.md, skills/presentations.md, axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md)\n", + "content": "# iPhone Duo\n\niPhone Duo is Apple's first two-display iPhone: an outer display, and a larger inner display that folds along a hinge. This skill covers adapting an app to it — the device model, what changes with the SDK you build against, vertical bars, the fold, and the new layout, hinge, and scene tools. SwiftUI comes first; the UIKit equivalent sits beside it.\n\n## SDK Status — Read First\n\nApple announced the Duo-specific APIs in six tech talks (111461–111466, September 2026). They shipped in the **iOS 27.1 SDK** (and are in the 27.2 SDK); every snippet below is compiled against it. Gate 27.1 calls at `@available(iOS 27.1, *)` and keep the pre-27.1 path compiling.\n\n- **Check the installed SDK first** (`xcrun --sdk iphoneos --show-sdk-version`). Below 27.1 the 27.1 symbols don't exist — describe the API, name the talk, and give today's alternative. On 27.1 or later: grep the SDK's `.swiftinterface` and headers for the name; the SDK's spelling and signature win over this file; if it's missing, check for a rename before calling it dropped.\n- **Never call iPhone Duo or these APIs fictional or hallucinated.** They come from Apple's own tech talks and are in the shipped SDK.\n- **Never invent parameters, types, or cases** beyond what the snippets and the SDK give.\n\n## When to Use This Skill\n\nUse when:\n- Preparing an app for iPhone Duo, a foldable iPhone, or a two-display iPhone\n- Layout breaks in some poses — closed, open, rotated, or partially folded\n- Toolbar, tab bar, or navigation bar items should move to the side of the screen\n- Interactive UI lands in the fold or under the inner camera\n- Choosing between reserved regions, arrangements, and the hinge\n- Showing content on another display, or opening multiple windows on iPhone\n- Code or a question names a 27.1 API (`onHingeChange`, `ArrangementView`, `reservedRegions`, `axisBehavior`, …)\n\n#### Related Skills\n- axiom-uikit (skills/uikit-modernization.md) — the resizing baseline Duo builds on: scene lifecycle, geometry, size classes\n- skills/layout.md — adaptive layout; the size-class truth tables include Duo\n- skills/toolbars.md — SwiftUI placements, overflow, and visibility priority\n- axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md) — Duo front cameras and camera direction\n\n## Example Prompts\n\n#### 1. \"How do I prepare my app for iPhone Duo?\"\n→ Readiness Today: resizing baseline, per-side safe areas, standard containers; build with the 27.1 SDK or later.\n\n#### 2. \"How do I detect that my app is running on iPhone Duo?\"\n→ Don't. It's still an iPhone app — use size classes and scene geometry.\n\n#### 3. \"My toolbar buttons should move to the side on iPhone Duo.\"\n→ Vertical Bars: system-managed bars only, titles on every item, overflow priorities.\n\n#### 4. \"A button sits in the fold when the phone is partly closed.\"\n→ The Fold and the Camera: displacement rules.\n\n#### 5. \"Can I use the hinge angle in my app?\"\n→ Hinge: effects and interactions, never layout.\n\n#### 6. \"Add .axisBehavior(.horizontalOnly) to my Select button\"\n→ Vertical Bars: the axis APIs are iOS 27.1; a build against 27.0 or earlier never gets vertical bars — ship titles, images, and priorities until you rebuild.\n\n## Red Flags — Anti-Patterns to Prevent\n\n| Thought | Reality |\n|---|---|\n| \"I'll check the model identifier and give Duo its own layout\" | It's still an iPhone app — compact width outside, regular width inside. Model and idiom checks break in Split View, in iPhone Mirroring, and on the next device. Use size classes and scene geometry. |\n| \"I'll branch on interface orientation\" | The inner display ignores your supported orientations. Decide layout with size classes. |\n| \"`UIScreen.main` gives me the screen\" | Deprecated since iOS 26, and ambiguous with two displays. Use `window?.windowScene?.screen` or `traitCollection.displayScale`. |\n| \"Safe-area insets are symmetric\" | A vertical bar sits on one side, so left ≠ right — and in Split View it switches sides. Inset each side independently. |\n| \"I'll build my own bottom bar\" | A custom `UIToolbar`/`UINavigationBar`/`UITabBar` or a hand-built SwiftUI row never moves to the side, and a hand-built row gets none of the system's fold avoidance. Use system-managed bars. |\n| \"I'll hide the controls when it's folded\" | Displace, never hide: move, resize, or reorganize so every function stays reachable in every pose. |\n| \"I'll read the hinge angle to size my panes\" | The hinge drives effects and interactions. Layout uses arrangements and reserved regions. |\n| \"Each pose gets its own layout\" | Design for the two horizontal size classes — compact outside, regular inside. An optional tabletop layout must keep every control and the same hierarchy. |\n| \"It's on the inner display, so it's wide\" | A Split View half reports compact width, like the outer display (measured on the 27.1 Duo simulator). Read the size class from the environment; never key a wide layout to the display. |\n| \"The New Window button can always show\" | The outer display can't create windows. Gate the affordance. |\n\n## The Device\n\n| Display | Horizontal | Vertical |\n|---|---|---|\n| Outer, portrait | `.compact` | `.regular` |\n| Outer, landscape | `.compact` | `.compact` |\n| Inner, full screen | `.regular` | `.regular` |\n| Inner, one half of Split View | `.compact` | `.regular` |\n\n- **Poses** Closed; open in portrait or landscape; partially folded like a book; seated like a laptop (tabletop) with the inner display facing you; standing on its edges.\n- **Still an iPhone app** Adapt to size classes and scene bounds, never to the device.\n- **Controls on the side** Built against the 27.1 SDK, in every pose except inner-display portrait, bars lay out vertically along the side, sharing that edge with the status bar, the Dynamic Island, and Live Activities. When space runs out, items collapse into the overflow menu.\n- **Multitasking** A 50/50 split view places two apps side by side, each with its controls on its outer edge (`toolbarVerticalEdge` reads `.leading` left, `.trailing` right) and the compact-width layout in each half. Picture in Picture can pin to the top; the app below resizes vertically.\n- **Offset, don't center** Most content offsets away from the side controls — align to horizontal safe-area insets and it happens for you. Center on the full display only for non-scrolling, highly visual UI whose interactive elements the controls can't cover. A full-width background under inset scrolling content also works.\n- **Inner display** Don't stretch the iPhone layout. Use a split view, a two-column rearrangement when width allows, or a tab sidebar for information-dense apps. Keep the hierarchy identical inside and out — people open and close the device mid-task.\n- **Games** Lock to portrait or landscape, but fill the screen in every pose; change the aspect ratio rather than letterboxing or pillarboxing (HIG).\n\n## Behavior by the SDK You Build Against\n\nLink-time behavior — what the device does with your binary:\n\n| Built against | On iPhone Duo |\n|---|---|\n| Pre-27 SDK | Closed: runs in the space beside the status bar and camera. Open: a familiar size and aspect ratio |\n| iOS 27.0 SDK | Resizes like any 27 iPhone app; extends left of the status bar on the inner display |\n| iOS 27.1 SDK | Edge to edge; standard navigation, toolbar, and tab bars lay out vertically |\n\n`UIRequiresFullScreen` is still honored, but the app still resizes when the device opens or closes. Supported orientations govern the outer display as on any iPhone; the inner display doesn't honor them — the app scales there instead, including in Split View.\n\n## Which Tool When\n\n| Need | Tool | Availability |\n|---|---|---|\n| Navigation, tabs, sheets, alerts, and menus that adapt to every pose | Standard containers (`NavigationSplitView`, `TabView`, `UISplitViewController`, …) | Today |\n| Custom UI that must avoid the fold or the inner camera | Reserved regions | 27.1 |\n| Two views that split side by side or overlay | Arrangements | 27.1 |\n| An effect or interaction driven by the fold angle | Hinge | 27.1 |\n| Extra content on another display | Scene accessories | Today for external displays; Duo camera variant 27.1 |\n| A second window of your app | Multiple scenes, inner display only | Today |\n\n## Readiness Today\n\nThe resizing baseline is the same as for every 27 iPhone app — scene lifecycle, no `UIScreen.main`, size classes over orientation and idiom: axiom-uikit (skills/uikit-modernization.md). Adaptive patterns: skills/layout.md. Duo adds five things.\n\n#### Handle each safe-area side independently\n\n```swift\n// Wrong on Duo: a vertical bar on one side makes left != right\nlet assumedWidth = view.bounds.width - view.safeAreaInsets.left * 2\n// Right: inset each side on its own\nlet contentWidth = view.bounds.inset(by: view.safeAreaInsets).width\n```\n\nLayout margins are asymmetric too. SwiftUI places content inside the safe area by default; let backgrounds extend with `.ignoresSafeArea()`, and in UIKit size backgrounds to `view.bounds`. With Auto Layout, constrain to `safeAreaLayoutGuide` / `layoutMarginsGuide` — each side stays independent (axiom-uikit (skills/adaptive-layout.md)). Under right-to-left languages the bar stays on the hardware side, so never assume the trailing inset is the larger one.\n\n#### Offer a tab sidebar on the inner display `iOS27`\n\n```swift\nTabView {\n Tab(\"Summary\", systemImage: \"heart\") { SummaryView() }\n Tab(\"Browse\", systemImage: \"square.grid.2x2\") { BrowseView() }\n}\n.tabViewStyle(.sidebarAdaptable) // required: the placement applies only to this style\n.defaultTabBarPlacement(.sidebar)\n```\n\nUIKit: `tabBarController.sidebar.preferredPlacement = .sidebar`. The SwiftUI modifier has no effect on iPadOS, where the bar adapts on its own — use `defaultAdaptableTabBarPlacement(_:)` there. A sidebar suits information-dense apps; most apps keep the tab bar.\n\n#### Gate new-window affordances\n\niPhone Duo is the first iPhone with multiple windows of one app, and only its inner display can create them. Both paths need `UIApplicationSupportsMultipleScenes` set to `YES` in the scene manifest (axiom-uikit (skills/uikit-modernization.md)), and `openWindow(id:value:)` needs a matching `WindowGroup(id:for:)` (axiom-design (skills/app-composition.md)). Apple's doc comment for `supportsMultipleWindows` limits `true` to macOS and iPadOS with that key — but on iPhone Duo the key is what decides: measured on the 27.1 Duo simulator, the value is `true` on the closed outer display and the open inner display alike, and it does not change as the device opens or closes; without the key it is `false`. So it reports that the manifest is configured, not which display can create windows — rely on the system's own control (`UIWindowScene.ActivationAction`, which hides itself where new windows aren't available) rather than gating on this value alone.\n\n```swift\nstruct ItemRow: View {\n let item: Item\n @Environment(\\.supportsMultipleWindows) private var supportsMultipleWindows\n @Environment(\\.openWindow) private var openWindow\n\n var body: some View {\n Text(item.title)\n .contextMenu {\n if supportsMultipleWindows {\n // true on both Duo displays; the request still fails on the outer one\n Button(\"Open in New Window\") { openWindow(id: \"detail\", value: item.id) }\n }\n }\n }\n}\n```\n\n```swift\n// UIKit menu action (iOS 15): where new windows aren't available the alternate runs;\n// with no alternate, the talks say the item hides (111464)\nlet openHere = UIAction(title: \"Open\") { _ in showDetailInCurrentWindow() }\nlet newWindow = UIWindowScene.ActivationAction(alternate: openHere) { _ in\n UIWindowScene.ActivationConfiguration(userActivity: detailActivity)\n}\n\n// Direct request: fails on the outer display, so handle the error (iOS 17)\nlet request = UISceneSessionActivationRequest(role: .windowApplication, userActivity: detailActivity)\nUIApplication.shared.activateSceneSession(for: request) { error in\n logger.error(\"Window request failed: \\(error.localizedDescription, privacy: .public)\")\n showDetailInCurrentWindow()\n}\n```\n\nThe Swift name is `UIWindowScene.ActivationAction`; the ObjC name `UIWindowSceneActivationAction` doesn't compile in Swift. The value is pose-independent (measured on the 27.1 Duo simulator), so the affordance doesn't need rebuilding as the device folds — and where new windows aren't available, `UIWindowScene.ActivationAction` hides itself.\n\n#### Match the new corners and support landscape\n\n`ConcentricRectangle` / `UICornerConfiguration` (iOS 26) are updated for Duo's display corners — skills/26-ref.md (Corner Concentricity). Support landscape on the outer display; people may set the phone down like a tent.\n\n#### Test both halves of Split View\n\nDrag the app to the left half, then the right. The vertical bar follows the app's outer edge, so the larger safe-area inset switches sides — `toolbarVerticalEdge` reads `.leading` in the left half and `.trailing` in the right (measured on the 27.1 Duo simulator). A half reports **compact width / regular height**, the same width class as the outer display, so the compact layout is what people see in Split View; read it from the environment rather than keying a wide layout to the display.\n\n## Vertical Bars\n\nBuilt against the 27.1 SDK, navigation, toolbar, and tab bar items share one vertical stack along the side — picture the horizontal bars rotated 90°. Only **system-managed bars** take part: `NavigationStack`, `NavigationSplitView`, or `TabView` with `.toolbar`, or `UINavigationController` and `UITabBarController` with items set on the view controller. The content of a custom `UIToolbar`, `UINavigationBar`, or `UITabBar` never joins the vertical bar; it stays where you put it.\n\n#### Which bars go vertical\n\n- Only a split view's detail column; other columns keep horizontal bars. Inspectors get no vertical bar.\n- Sheets: on the outer display a sheet's toolbar goes vertical by default (disable it with the off switch below); on the inner display sheets center with horizontal bars. A sheet placed on the right gets a vertical bar; one on the left doesn't. The placement APIs — `.presentationPlacement(.trailing)` and `sheetPresentationController?.preferredPlacement = .trailing`, both iOS 27; the UIKit one is ignored when `sourceView` is set — take leading/trailing, but the rule is physical right/left.\n- Controls that belong to a content area stay with it, not on the side — Mail keeps the list's controls above the leading pane (HIG).\n- The bar stays on the hardware side under right-to-left languages; content adapts around it.\n- Keyboard accessory bars stay on the keyboard.\n- The inner display in portrait keeps horizontal bars.\n\n#### Host the bar in a system-managed container\n\nItems join the vertical bar only from inside one of the system-managed containers above. The same `.bottomBar` items on a view with none of them — in a bare `fullScreenCover`, say — render as a horizontal capsule along the bottom, while `toolbarVerticalEdge` still reads `.trailing` (measured on the 27.1 Duo simulator, closed). Adding a `NavigationStack` to a custom full-screen layer brings two side effects:\n\n- **The stack paints an opaque background** over whatever is behind it. Apply `.containerBackground(.clear, for: .navigation)` to the content *inside* the stack; applied to the `NavigationStack` itself it had no effect (both measured).\n- **The stack gives its content the window's safe-area insets back**, even under an ancestor's `.ignoresSafeArea()` — measured trailing 84 pt and bottom 34 pt on the closed Duo, where the content outside the stack read zero. Re-apply `.ignoresSafeArea()` inside the stack if the layout depends on reading zero.\n\n```swift\nstruct PlayerCover: View {\n var body: some View {\n ZStack {\n ArtworkBackground()\n NavigationStack {\n PlayerControls()\n .ignoresSafeArea()\n .toolbar(.hidden, for: .navigationBar)\n .containerBackground(.clear, for: .navigation) // inside the stack, not on it\n .toolbar {\n ToolbarItem(placement: .bottomBar) {\n Button(\"Shuffle\", systemImage: \"shuffle\") { }\n }\n }\n }\n }\n .ignoresSafeArea()\n }\n}\n```\n\n#### Order items top to bottom\n\n1. Back (automatic in a navigation container) or a custom close. SwiftUI: `.cancellationAction`. UIKit: a leading item, with `leftItemsSupplementBackButton` left `false` (the default).\n2. The prominent action. SwiftUI: `.topBarPinnedTrailing` `iOS27`. UIKit: `pinnedTrailingGroup`.\n3. Everything else, in its existing groups. A spacer separates top and bottom placements.\n\nTop-bar items go to the top of the stack, bottom-bar items to the bottom, and the tab bar stays bottom-aligned.\n\nKeep placement consistent across poses so people don't relearn where actions live. SwiftUI code for these placements and for overflow: skills/toolbars.md (Pattern 2, Pattern 11). UIKit:\n\n```swift\nnavigationItem.leftItemsSupplementBackButton = false // the custom close replaces back\nnavigationItem.leadingItemGroups = [UIBarButtonItemGroup(barButtonItems: [closeItem], representativeItem: nil)]\nnavigationItem.pinnedTrailingGroup = UIBarButtonItemGroup(barButtonItems: [doneItem], representativeItem: nil)\nnavigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { completion in\n completion([UIAction(title: \"Scan\", image: UIImage(systemName: \"doc.viewfinder\")) { _ in scan() }])\n}\nshareItem.visibilityPriority = .high // iOS27: collapses after standard-priority items\ninboxItem.badge = .count(7) // iOS 26: a symbol-only item that still shows the count\n```\n\n`UIBarButtonItem.Badge` is itself main-actor isolated — even `.count(n)` can't be built in a nonisolated model.\n\n#### Make items vertical-ready\n\n- Give every item a title and an image — a SwiftUI `Label`, or a `UIBarButtonItem` with both. Bars show the icon; the overflow menu shows title and icon.\n- Items with an icon go vertical; text-only items stay horizontal. An item that switches between a symbol and text (a custom Select/Done) belongs on the horizontal axis — the system edit button already stays there.\n- Replace inline counts with a badge. Text that carries real information, like a cart total, stays in a horizontal bar.\n- Custom views, complex views, and wide controls like segmented controls stay horizontal unless opted in (Axis, edge, compression, and the off switch).\n- Vertical bars have a fixed width and flexible height; once a custom view opts in, it must fit that width or adapt its layout. Flexible spacers collapse to zero vertically; fixed spacers keep their minimum. Don't add extra spacing — group items with `ToolbarItemGroup` / `UIBarButtonItemGroup`, which supply it and adapt it.\n- Vertical bars have no scroll-edge effect but gain a background under Reduce Transparency — keep custom content legible either way.\n- A hero or background image extends under the vertical bar with `.backgroundExtensionEffect()` (SwiftUI) or `UIBackgroundExtensionView` (UIKit).\n\n#### What your styling survives\n\n- **System items ignore your styling.** In the vertical bar a system `Toggle` or `Button` item ignores `.foregroundStyle` and `.opacity`; a `Toggle` shows \"on\" as a lit disc. `.contextMenu` on a system `Toggle` item does nothing (measured on the 27.1 Duo simulator).\n- **A custom view keeps it (measured).** Opt the item in with `.axisBehavior(.verticalPreferred)` and its own `.foregroundStyle`, `.opacity`, and `.contextMenu` all work in the bar — the route for on/off/unavailable states the system rendering can't show.\n- **`Menu(primaryAction:)` shows no on/off state** in the bar (reported, not re-measured).\n- **The overflow menu shows each item's title and icon**, taken from its `Label`, but **no on/off state and no context menu** (reported). If state matters once an item overflows, put it in the title (\"Repeat One\").\n\n#### Plan for overflow\n\nThe outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (Axis, edge, compression, and the off switch). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up; `.visibilityPriority(.high)` works on custom items too (measured: of ten custom items in a seven-slot bar, the last two marked `.high` stayed and the three above them overflowed). A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar.\n\n#### When to turn vertical bars off\n\nA single-page, bottom-heavy layout like a calculator, or a sheet whose only item is Close, may work better with horizontal bars.\n\n#### Axis, edge, compression, and the off switch\n\nThe inferred axis is usually right — a title-only item stays horizontal, an item with an image goes vertical. Override it per item when a custom view, a wide control, or a symbol↔text toggle needs a specific axis. All of these are iOS 27.1; below that, items keep whatever axis the system infers, and the knobs don't exist. A build against the 27.0 SDK or earlier never sees a vertical bar at all.\n\n`toolbarVerticalEdge` (UIKit: `traitCollection.verticalBarEdge`) reports the system's *preferred* edge \"regardless of whether a vertical bar is currently visible\", and is nil (`.unspecified`) where the system never places one — hardware without a vertical bar, or a size class or orientation that doesn't use it (UIKit header doc). What the value does and doesn't tell you:\n\n- It is set before any toolbar item exists, and reads the same inside and outside a navigation container (measured).\n- It is nil on the open inner display in portrait, half-folded included, and reads nil briefly during the opening transition (reported). Don't animate on a transient nil.\n- A non-nil edge doesn't mean *your* items are vertical — outside a system-managed container they stay horizontal (Host the bar in a system-managed container).\n\n```swift\n// SwiftUI — iOS 27.1: per-item axis override, compression order, and the edge read\n@available(iOS 27.1, *)\nstruct BarControls: View {\n @Environment(\\.toolbarVerticalEdge) private var edge: HorizontalEdge?\n\n var body: some View {\n NavigationStack {\n SummaryView()\n .toolbar {\n ToolbarItem {\n Button(\"Select\", systemImage: \"checkmark.circle\") { }\n }\n .axisBehavior(.horizontalOnly) // symbol↔text toggles and wide controls stay horizontal\n ToolbarItem {\n Button(\"Compass\", systemImage: \"location.north.circle\") { }\n }\n .axisBehavior(.verticalPreferred) // opt a custom view in (icon items infer this already)\n }\n .toolbarVerticalCompressionBehavior(.prefersToolbarItems) // tab bar compresses first\n .overlay(alignment: .bottom) {\n // .leading / .trailing where the system places bars vertically; nil where it never does\n Text(edge == .trailing ? \"Bar: trailing\" : edge == .leading ? \"Bar: leading\" : \"No vertical bar\")\n .font(.caption)\n }\n }\n }\n}\n\n@available(iOS 27.1, *)\nstruct HorizontalBarsOnly: View {\n var body: some View {\n SummaryView()\n .toolbarVerticalBehavior(.disabled) // bottom-heavy screens keep horizontal bars\n }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the item's axis, the compression preference, the off switch, the edge\n@available(iOS 27.1, *)\nfinal class PlayerViewController: UIViewController {\n override var preferredVerticalBarBehavior: UIVerticalBarBehavior { .disabled }\n}\n\n@MainActor\nfunc tuneVerticalBar(_ item: UIBarButtonItem, _ navigationItem: UINavigationItem, _ traits: UITraitCollection) {\n guard #available(iOS 27.1, *) else { return } // below 27.1 the knobs don't exist; bars stay horizontal\n item.axisBehavior = .verticalPreferred // .horizontalOnly keeps a wide custom view put\n navigationItem.verticalBarCompressionBehavior = .prefersBarItems // keep the item; compress the tab bar\n if traits.verticalBarEdge == .trailing {\n // .leading / .trailing / .unspecified — inset the side that holds the bar\n }\n}\n```\n\n## The Fold and the Camera\n\nWhen iPhone Duo is partially folded, the display curves through the center and splits into regions. Two kinds of **reserved region** shape the usable space:\n\n- **Division** — the fold. Active only while partially folded; zero width when flat. The element is a `ReservedRegion` (SwiftUI) or `UIView.ReservedRegion` (UIKit); both expose `frame`, `margins`, and `isActive`.\n- **Occlusion** — the inner FaceTime camera. Active only while that camera runs.\n\nThe outer display's camera is always present — it shares the corner with the Dynamic Island, which expands for Live Activities — and the system accounts for it, including when bars go to the side (HIG).\n\n#### Displacement rules\n\n- Move, resize, or reorganize — never hide. Every function stays reachable in every pose.\n- Move elements that work together as a unit; move independent elements alone.\n- Avoid long moves; distance weakens the link between an element and its source.\n- Scrolling content — articles, feeds, lists — never displaces; it already adapts by scrolling.\n- Let purpose choose the destination. Book pose: alerts move to the trailing side, where they'll be when the device closes. Tabletop: content meant to be seen from a distance goes to the top region; tappable controls go to the bottom, a stable surface.\n- Stay contextual: search stays over the view it searches.\n\nSystem components already avoid the fold: sheets, alerts, action sheets, menus, popovers, and toolbar buttons, and split views rebalance to an even 50/50. Use them wherever you can.\n\n#### Custom grids\n\nThese change spacing and column count, not which region content lives in, so they aren't displacement.\n\n- **Keep each item inside one region while folded.** Preserve the outer margins and widen the spacing around the fold (Apple's Fitness example, 111463 5:56).\n- **Consider an even column count.** Apple suggests preferring an even number of columns when a division region exists, active or not (111463 7:36). The middle gap lands on the fold only when the grid is centered on the display and the fold runs vertically through it, as in book pose; otherwise place the gap from the region's `frame`. `GridItem(.adaptive(minimum:))` picks its own count, which can be odd.\n- **Find the fold.** Read the division region's `frame`, passing `.includeInactive` for the column decision. Below 27.1 only system components know where the fold is: don't hard-code the display's midpoint or check the device model.\n\n```swift\n// SwiftUI — iOS 27.1: the fold's and the camera's regions, in view coordinates\n@available(iOS 27.1, *)\nfunc foldGap(proxy: GeometryProxy) -> CGFloat {\n proxy.reservedRegions(kind: .division, options: .includeInactive)\n .filter(\\.isActive)\n .map(\\.frame.width)\n .max() ?? 0\n}\n\n// UIKit — iOS 27.1\n@MainActor\nfunc cameraOcclusionRect(in view: UIView) -> CGRect? {\n guard #available(iOS 27.1, *) else { return nil }\n return view.reservedRegions(kind: .occlusion).first?.frame\n}\n```\n\n## Arrangements\n\nAn arrangement places a primary and a secondary view by rules — size classes, aspect ratio, and active fold regions. It sits between navigation containers and content containers.\n\n- **Split** — main and detail content where neither view may be obscured, like a player and its transcript. The default style; it splits along the longer axis unless restricted. If the split can't use the view's long axis (e.g. `.axes(.horizontal)` in a tall view), it shows a single view (the primary, in the talk's example) — keep the secondary reachable another way.\n- **Overlay** — a clear foreground and background, like controls over readable content. Closed or fully open, it layers the primary over the secondary; partially folded, the primary moves to the trailing (or bottom) region and the secondary to the leading (or top).\n- Follow existing patterns: an HStack or VStack split becomes a split arrangement; a ZStack overlay becomes an overlay arrangement.\n- Never nest a navigation container inside an arrangement, and never put an arrangement inside a `List` or `ScrollView`.\n- Put it inside the navigation container — the talks nest it in a `NavigationStack` and make the UIKit controller the navigation root. Use it for split-like layout without a split view's expand/collapse.\n\nThe API is iOS 27.1; below that, use `NavigationSplitView` (or nested stacks) for the same jobs — arrangements don't exist there.\n\n```swift\n// SwiftUI — iOS 27.1: split or overlay by rules\n@available(iOS 27.1, *)\nstruct PlayerScreen: View {\n var body: some View {\n NavigationStack {\n ArrangementView {\n PlayerView()\n } secondary: {\n UpNextView()\n }\n .arrangementViewStyle(.split.axes(.horizontal)) // or .overlay\n }\n }\n}\n\n@available(iOS 27.1, *)\nstruct PlayerView: View {\n var body: some View { Text(\"Now playing\") }\n}\n\n@available(iOS 27.1, *)\nstruct UpNextView: View {\n @Environment(\\.overlayArrangementZIndex) private var zIndex: Int // changes as the device folds\n var body: some View { Text(zIndex > 0 ? \"Collapsed\" : \"Expanded\") }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the arrangement controller is the navigation root\n@available(iOS 27.1, *)\n@MainActor\nfinal class ArrangementHost {\n let controller = UIArrangementViewController()\n\n func place(player: UIViewController, upNext: UIViewController) {\n controller.setViewController(player, for: .primary)\n controller.setViewController(upNext, for: .secondary)\n controller.updateArrangement(.split.axes(.horizontal))\n }\n\n func primaryZIndex() -> Int {\n controller.state(for: .primary)?.zIndex ?? 0\n }\n}\n```\n\n## Hinge\n\nThe hinge reports a status — closed, partially open, fully open — and a continuous angle. Use it for effects and interactions, like a pitch bend or a zoom that follows the fold, never for layout. A missing hinge means the device has none; reset hinge-driven state whenever the device isn't partially open. The API is iOS 27.1; below that your app never sees hinge updates.\n\n```swift\n// SwiftUI — iOS 27.1\n@available(iOS 27.1, *)\nstruct HingeDrivenView: View {\n @State private var bend = Angle.zero\n\n var body: some View {\n Text(\"Pitch bend\")\n .rotationEffect(bend)\n .onHingeChange { _, new in\n if let hinge = new.hinge, hinge.status == .partiallyOpen {\n bend = hinge.angle\n } else {\n bend = .zero // no hinge, or not partially open: reset\n }\n }\n }\n}\n```\n\n```swift\n// UIKit — iOS 27.1: the same state, delivered to an interaction\n@available(iOS 27.1, *)\n@MainActor\nfinal class HingeObserver {\n private var interaction: UIHingeInteraction?\n\n func attach(to view: UIView, onChange: @escaping (UIHinge.Status, CGFloat) -> Void) {\n let interaction = UIHingeInteraction { _, update in\n guard let hinge = update.hinge else { return } // nil: this hierarchy provides no hinge\n onChange(hinge.status, hinge.angle) // angle is in radians\n }\n view.addInteraction(interaction)\n self.interaction = interaction\n }\n}\n```\n\n## Scenes and Accessories\n\nA **scene accessory** is supplementary content the system presents for you when a capability becomes available. The system decides when and where it appears, and your app must stay fully functional without it.\n\n#### External-display accessory `iOS27`\n\nToday's accessory targets an external display, connected or over AirPlay:\n\n```swift\nstruct PresenterView: View {\n let deck: Deck\n @State private var showsAudienceView = true\n @State private var accessoryAvailable = false\n\n var body: some View {\n SlideEditor(deck: deck)\n .toolbar {\n ToolbarItem {\n Toggle(\"Audience View\", systemImage: \"rectangle.on.rectangle\", isOn: $showsAudienceView)\n .disabled(!accessoryAvailable)\n }\n }\n .sceneAccessory {\n ExternalNonInteractiveAccessory(isEnabled: $showsAudienceView) {\n AudienceSlide(deck: deck)\n }\n .onAvailabilityChange { accessoryAvailable = $0 }\n }\n }\n}\n```\n\nRegister the accessory on the view whose visibility should gate it.\n\n#### The Duo camera accessory\n\nOn iPhone Duo, a camera variant shows UI on the outer display — a teleprompter, or something to show the person being photographed — while your camera UI runs on the inner display. It's available only while the app is full screen on the inner display with an active camera session, and it's iOS 27.1 — below that the type doesn't exist, so ship without it. Camera direction and the new front cameras: axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md).\n\n```swift\n// SwiftUI — iOS 27.1: outer-display UI while an inner-display camera session runs\n@available(iOS 27.1, *)\nstruct CameraScreen: View {\n @State private var showsOuterUI = true\n @State private var accessoryAvailable = false\n\n var body: some View {\n Text(\"Camera preview\")\n .sceneAccessory {\n CameraCaptureAccessory(isEnabled: $showsOuterUI) {\n Text(\"Teleprompter\")\n }\n .onAvailabilityChange { accessoryAvailable = $0 }\n }\n }\n}\n```\n\nUIKit registers the same content on the capture view controller and holds the returned registration:\n\n```swift\n// UIKit — iOS 27.1\n@available(iOS 27.1, *)\n@MainActor\nfinal class ScriptSceneDelegate: NSObject, UIWindowSceneDelegate {\n func scene(_ scene: UIScene, willConnectTo session: UISceneSession,\n options connectionOptions: UIScene.ConnectionOptions) {\n // connectionOptions.sceneAccessoryUserInfo carries the object you pass as userInfo, when you pass one\n }\n}\n\n@available(iOS 27.1, *)\n@MainActor\nfinal class CameraViewController: UIViewController {\n private var registration: UISceneAccessoryRegistration?\n\n override func viewDidLoad() {\n super.viewDidLoad()\n let configuration = UISceneConfiguration()\n configuration.delegateClass = ScriptSceneDelegate.self\n registration = registerSceneAccessory(\n .cameraCapture(sceneConfiguration: configuration)\n )\n }\n}\n```\n\nAvailability (`isAvailable`) and your on/off switch (`isEnabled`) are separate: the system decides when it can present, you decide whether to offer. Content appears only while the app is foreground, capturing, and on the inner display; it yields to the top-most registration of its kind, and goes away when capture stops, the app backgrounds, or the device closes — keep essential controls on the inner display and treat the accessory as an enhancement. Test on hardware; Simulator has no camera. Apple article: \"Registering a camera capture accessory on iPhone Duo\".\n\n## Tooling and Testing\n\n- **Device Hub** — Xcode 27.1's Device Hub drives an iPhone Duo simulator with open, close, rotate, and fold controls (111461 0:56); Apple's overview notes the Duo simulator in Device Hub requires Xcode 27.1. The device type creates against the **iOS 27.1 runtime**; the 27.0 runtime rejects it (`Incompatible device`). `iPhone Fold` is a different product.\n- **The 27.1 runtime is Duo-only.** Creating iPhone 17, 17 Pro, 17e, or iPad Pro on it fails with the same `Incompatible device` (SimError 403), so keep the 27.0 runtime for every other device (measured with Xcode 27.1).\n- **Poses are Device Hub-only.** No `simctl` or `devicectl` command opens, closes, or folds the simulator. `devicectl device motion hinge-angle` only *reads* the hinge, and `devicectl device orientation set` prints success and changes nothing on the Duo (measured closed; reported open). Check the window size after every pose change: closing from open-landscape has been reported landing in closed-portrait one time and closed-landscape another.\n- **Screenshots default to the inner display**, which is black while the device is closed — for `simctl io … screenshot` and `devicectl device capture` alike. Capture the outer display by name: axiom-tools (skills/device-control-ref.md, Display masks and multi-display devices).\n- **Name closed poses by the hinge**, never by \"turned left/right\". Closed and upright, the hinge is the left edge, the camera and status bar sit top-right, and the vertical bar runs down the trailing edge (`toolbarVerticalEdge == .trailing`, measured). With the hinge along the bottom the camera is top-left and the bar moves to the leading edge; hinge along the top puts the camera bottom-right and the bar trailing (reported). The bar follows the camera's edge.\n- **Synthetic taps** — send a physical touch (`xcui tap` does; bare `axe tap` needs `--tap-style physical`), or the bar's items ignore the tap while it reports ✓: axiom-tools (skills/xcui-ref.md, Tap styles). The bar's overflow (⋯) button did not open under any tap style or a 0.2–1.0 s hold in the closed pose (measured); test overflowed actions in a pose with room for them.\n- **Simulator gaps** — per the Xcode 27.1 beta release notes, StandBy is unavailable in the iPhone Duo Simulator runtime, and running and debugging most app extensions is unavailable there.\n- **App Resizability** — Xcode's app-modernization agent skill, renamed \"App Resizability\", now covers SwiftUI and iPhone Duo (111461 9:15). See axiom-uikit (skills/uikit-modernization.md).\n\n## Pressure Scenarios\n\n#### \"Ship Duo support by Friday — just check for the Duo model\"\nA model check covers one device and breaks in Split View and iPhone Mirroring. Size classes and per-side safe areas take the same time and cover every pose. Push back: \"Size classes handle Duo and every future device; a model check handles one.\"\n\n#### \"That API doesn't exist — drop the Duo section\"\nThe APIs come from Apple's September 2026 tech talks and shipped in the iOS 27.1 SDK. Keep the guidance, verify each name against the installed SDK, and where the SDK you build with is older, ship today's alternatives and say what the 27.1 rebuild brings (the bar behavior is link-time).\n\n#### \"Just hide the controls when it's folded\"\nHiding ties functionality to a pose. Move the controls to the region that suits their purpose; system components already do this.\n\n## Checklist\n\n- ☑ No model, idiom, or orientation checks drive layout\n- ☑ No `UIScreen.main`; geometry comes from the scene or the view\n- ☑ Safe-area and margin math handles each side independently\n- ☑ Corner configuration matches Duo's display corners; the outer display supports landscape\n- ☑ Bars are system-managed; every item has a title and an image\n- ☑ Items run back/close → prominent → the rest; overflow priorities are set\n- ☑ New-window affordances are gated; scene-request errors are handled\n- ☑ Interactive UI stays out of the fold through system components or displacement — never by hiding\n- ☑ Tested closed, open in both orientations, partially folded, and in both halves of Split View\n- ☑ 27.1 APIs carry `@available(iOS 27.1, *)`; the app still compiles against the older SDK you support\n\n## Resources\n\n**Tech Talks**: 111461, 111462, 111463, 111464, 111465, 111466\n\n**Docs**: /swiftui/view/defaulttabbarplacement(_:), /swiftui/view/sceneaccessory(content:), /swiftui/externalnoninteractiveaccessory, /swiftui/environmentvalues/supportsmultiplewindows, /uikit/uiwindowscene/activationaction, /uikit/uiapplication/activatescenesession(for:errorhandler:), /uikit/uinavigationitem/pinnedtrailinggroup, /technologyoverviews/preparing-your-app-for-iphone-duo, /design/human-interface-guidelines/designing-for-iphone-duo\n\n**Skills**: axiom-uikit (skills/uikit-modernization.md), skills/layout.md, skills/toolbars.md, skills/presentations.md, axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md)\n", "skillType": "discipline", "source": "axiom", "tags": [ @@ -38188,63 +38188,63 @@ "heading": "Vertical Bars", "level": 2, "startLine": 180, - "endLine": 293, - "charCount": 7986 + "endLine": 335, + "charCount": 11519 }, { "heading": "The Fold and the Camera", "level": 2, - "startLine": 294, - "endLine": 339, + "startLine": 336, + "endLine": 381, "charCount": 2994 }, { "heading": "Arrangements", "level": 2, - "startLine": 340, - "endLine": 398, + "startLine": 382, + "endLine": 440, "charCount": 2698 }, { "heading": "Hinge", "level": 2, - "startLine": 399, - "endLine": 440, + "startLine": 441, + "endLine": 482, "charCount": 1524 }, { "heading": "Scenes and Accessories", "level": 2, - "startLine": 441, - "endLine": 528, + "startLine": 483, + "endLine": 570, "charCount": 3722 }, { "heading": "Tooling and Testing", "level": 2, - "startLine": 529, - "endLine": 534, - "charCount": 762 + "startLine": 571, + "endLine": 581, + "charCount": 2551 }, { "heading": "Pressure Scenarios", "level": 2, - "startLine": 535, - "endLine": 545, + "startLine": 582, + "endLine": 592, "charCount": 854 }, { "heading": "Checklist", "level": 2, - "startLine": 546, - "endLine": 558, + "startLine": 593, + "endLine": 605, "charCount": 806 }, { "heading": "Resources", "level": 2, - "startLine": 559, - "endLine": 566, + "startLine": 606, + "endLine": 613, "charCount": 716 } ], @@ -40872,7 +40872,7 @@ "axiom-tools--device-control-ref": { "name": "axiom-tools--device-control-ref", "description": "The Xcode-independent surface for driving simulators and physical devices: which tool owns", - "content": "# Device Control Reference — Device Hub, devicectl, simctl\n\nThe Xcode-independent surface for driving simulators and physical devices: which tool owns\nwhat, and what each one costs to run. Device Hub (the Xcode 27 GUI) is a front-end over\n`devicectl`/`simctl` — every operation has a scriptable, headless counterpart, so a full\ndev/CI loop needs no running Xcode.\n\n## Tool map — what each owns, and what it needs\n\n| Tool | Owns | Needs Xcode running? |\n|------|------|----------------------|\n| `devicectl` (CLI) | configure + interact with a booted sim OR physical device through one `-d ` selector; install/launch/inspect; capture screenshots + screen recordings; status-bar overrides incl. a `screenshot` preset; drive free resize (`appResize`) `OS27`; stable `--json-output` | No |\n| `simctl` (CLI) | simulator lifecycle (create/boot/shutdown/erase) + sim-only state: push, privacy permissions, media, `openurl`, `ui appearance`; shares the `status_bar` override store with devicectl and solely owns `--operatorName` | No |\n| `xcui` (Axiom) | drive in-app UI + accessibility tree (tap/assert, VoiceOver order); toggle a11y settings | No |\n| `xclog` (Axiom) | capture simulator/device console | No |\n| `xcsym` (Axiom) | symbolicate crashes (`.ips`, MetricKit, `.crash`) | No |\n| `xcprof` (Axiom) | record/analyze xctrace CPU & network profiles | No |\n| Device Hub (GUI) | visual front-end over devicectl/simctl — canvas, inspector; auto-launches on build-and-run | No (Xcode 27 installed, but needn't be open) |\n| `mcpbridge` (Xcode MCP) | IDE tools — build, test, render previews, project read | 26.x: **yes**. 27: no, via headless `mcp-server` (needs a `sudo` opt-in) `OS27` |\n\n**Answer to \"control the device without Xcode running\":** all of it.\n`devicectl` + `simctl` + Axiom's `xcui`/`xclog`/`xcsym`/`xcprof` cover the full scriptable\nsurface headlessly with no privilege escalation. On Xcode 27 the MCP bridge joins them —\n`sudo xcrun mcp-server enable` runs the tool service with Xcode.app closed\n(`axiom-xcode-mcp`) — so uptime is no longer what separates them `OS27`.\n\nPick on **capability and privilege** instead. `xcui` asserts, waits, toggles accessibility\nsettings, and computes VoiceOver announcements, and the whole CLI set runs unprivileged —\ndecisive in CI, where `sudo` is often unavailable. MCP owns what only the IDE knows: build\nstate and rendered previews. On Xcode 26.x the old rule still holds — `mcpbridge` alone\nneeds a running Xcode with a project open.\n\n## devicectl — the Core Device CLI\n\n`devicectl` (Xcode 15+, replaces the legacy `idevice*` tools) installs, launches, inspects, and\nconfigures devices from the command line. `xcrun devicectl list devices` returns a **unified\ninventory of physical devices *and* simulators**, distinguished by a `Reality` column\n(`physical` / `simulated`).\n\nThe pre-existing surface is materially identical across the 26 and 27 toolchains — same\nsubcommands and flags, verified against both (the exact binary build advances between beta seeds,\nso don't pin one). Xcode 27 adds: the `appResize` family (below), `devicectl device settings\nvoiceover`, and a service-side change letting `simctl` and `devicectl` reboot a simulator via\n`reboot`.\n\n```bash\n# Unified inventory: physical + simulated (--json-output for CI)\nxcrun devicectl list devices\n\n# Install / launch / inspect by identifier (sim UDID or device id — same -d)\nxcrun devicectl device install app --device MyApp.app\nxcrun devicectl device process launch --device com.your.bundleid\nxcrun devicectl device info apps --device \nxcrun devicectl device info processes --device \n```\n\n**Parse the structured `--json-output`, not the human-readable text.** devicectl guarantees the\nJSON is versioned and stable across releases; its human-readable output is explicitly *not* stable\n(simctl's human output never carried that guarantee either — the stability contract, not the\nunified `-d` syntax, is the real CI win).\n\n**Use the `properties` dictionary.** The older top-level `hardwareProperties`, `deviceProperties`,\nand `connectionProperties` keys are deprecated in favor of one `properties` dictionary and will be\nremoved; the `tags` key is already gone. Pass `--omit-deprecated-fields-in-json` to drop them now\nand prove a parser is forward-compatible. The fields worth keying off:\n\n| Field | Values | Always present |\n|---|---|---|\n| `properties.hardware.reality` | `physical` / `simulated` | Yes |\n| `properties.hardware.deviceType` | `iPhone`, `iPad`, `appleWatch`, … | Yes |\n| `properties.hardware.platform` | `iOS`, `watchOS`, … | Yes |\n| `properties.connection.state` | `connected` / `disconnected` / `unavailable` | Yes |\n| `properties.connection.pairingState` | `paired`, … | Yes |\n| `properties.state.developerModeStatus` | single-key dict: `{\"enabled\":{}}` / `{\"disabled\":{}}` | Physical only |\n| `properties.connection.transportType` | `sameMachine` (sim) / `localNetwork` / `wired` | **No** |\n| `properties.state.bootState` | `booted` / `shutdown` | **No** |\n\n**Read `reality` directly; never derive it from `transportType`.** `transportType` and `bootState`\nare absent on a physical device with no active connection — precisely the device you are debugging.\nA parser that infers physical-vs-simulated from `transportType` misclassifies or throws on exactly\nthe rows that matter. `hardware.reality` is a literal field and is always populated.\n\n`developerModeStatus` encodes an enum as a single-key dictionary, so test key presence\n(`\"enabled\" in status`), not a string compare. `connection.lastConnectionDate` is a number in this\nschema (seconds from the 2001 epoch) where the deprecated keys carried an ISO-8601 string — a\nsilent type change to catch when migrating.\n\nFor reading these states on an Apple Watch that will not appear or will not run, see\naxiom-watchos (`skills/watch-device-diag.md`).\n\n### Interaction vs lifecycle — devicectl does NOT replace simctl\n\ndevicectl **configures and interacts** with a booted device/sim; it has no `create`/`boot`/`erase`.\nsimctl still owns the simulator lifecycle and the sim-only features.\n\n| Need | Tool |\n|------|------|\n| create / boot / shutdown / erase a sim | `xcrun simctl boot\\|shutdown\\|erase` |\n| pick the test destination | `xcodebuild -destination` |\n| configure / interact with a booted sim or device | `xcrun devicectl` |\n| push, privacy permissions, media, openurl | `xcrun simctl` (sim-only) |\n| status bar overrides for screenshots | either — same store; `devicectl` adds a `screenshot` preset, `simctl` owns `--operatorName` |\n\nCI order is unchanged at the front: simctl or xcodebuild boots the sim → devicectl configures it\n→ run tests.\n\n### Simulator-capable subcommands (verified on Xcode 26.6 + 27.0)\n\n| Subcommand | On simulator | Use |\n|------------|--------------|-----|\n| `device info displays` | works (verified) | bounds, pointScale, nativeSize, `framebufferMaskIdentifier` (exact JSON keys) |\n| `device capture screenshot` / `screen-record` | works (verified) | PNG / H.264 `.mp4` capture, sim or device — see Screen capture below |\n| `device orientation get` (also `set`, `rotate`) | works (`get` verified) | orientation without entering the app |\n| `device settings biometrics [--enable\\|--disable]` | works (verified) | enroll / unenroll Face ID / Touch ID |\n| `device simulate biometrics --success\\|--failure` | works (verified) | drive a match / no-match |\n| `device settings appearance --mode light\\|dark` | works (verified) | force Dark/Light; also `--look-and-feel clear\\|tinted`, text size, contrast |\n| `device settings voiceover --enable\\|--disable` (also `device info voiceover`) | works (verified) `OS27` | toggle VoiceOver — the one a11y toggle `xcui` omitted for lack of a mechanism |\n| `device simulate location` | available | inject location |\n| `device simulate statusBar` (`preset`/`override`/`show`/`clear`) | works (verified) | clean status bar for screenshots — see Status bar for screenshots below |\n| `device process sendMemoryWarning` | available | memory-pressure scenarios |\n| `device info lockState` / `info files` / `copy` / `profile *` | physical-device-only | see caveat below |\n\n**Face ID / Touch ID is devicectl-only** — simctl has no biometric command (enrolling/matching was\na GUI-only Simulator menu, unscriptable):\n\n```bash\nSIM=$(xcrun simctl list devices booted | grep -Eo '[0-9A-F-]{36}' | head -1)\nxcrun devicectl device settings biometrics -d \"$SIM\" --enable # enroll\nxcrun devicectl device simulate biometrics -d \"$SIM\" --success # match (--failure for the reject path)\nxcrun devicectl device settings biometrics -d \"$SIM\" --disable # restore\n```\n\nThe flags are `--success` / `--failure` (mutually exclusive) — **not** `--match`.\n\n**Physical-device-only capabilities** on a simulator fail with a distinct, detectable error — not a\ncrash, not a silent no-op:\n\n```\nERROR: The capability \"Get Lock State\" is not supported by this device.\n (com.apple.dt.CoreDeviceError error 1001)\n```\n\n`info lockState` is confirmed device-only; `info files`, `copy`, and `profile *` are reported\ndevice-only on simulators. In CI, treat `CoreDeviceError 1001` as \"skip on simulator\", not a failure.\n\n## Screen capture — screenshot & video\n\n`devicectl device capture` is the **unified** capture path: one `-d ` selector across\nsimulators and physical devices, the same stable `--json-output`, and — for video — a\n`--duration` auto-stop that makes it the only script/CI-friendly recorder of the options here.\nPresent and verified on **both Xcode 26.6 and 27.0** (not new in 27 — another instance of the\n\"materially identical across 26 and 27\" CLI).\n\n```bash\n# Screenshot — destination MUST end in .png\nxcrun devicectl device capture screenshot -d --destination shot.png\n\n# Screen recording — destination MUST end in .mp4; --duration auto-stops (else Ctrl+C)\nxcrun devicectl device capture screen-record -d --destination clip.mp4 --duration 5\n```\n\n| Flag | screenshot | screen-record | Notes |\n|------|------------|---------------|-------|\n| `--destination` | `.png` only | `.mp4` only | wrong extension is a hard error, not a coercion |\n| `--display-unique-id` | yes | yes | pick from `device info displays`; omit = primary display |\n| `--codec` | — | `h264` (default), `hevc` | |\n| `--mask-policy` | — | `ignored` (default), `premultipliedAlpha`, `black` | bezel mask for non-rectangular displays |\n| `--duration ` | — | auto-stop after N seconds | omit = record until SIGINT |\n\nVerified on a booted iOS 26.5 simulator (screenshot → 1206×2622 PNG; screen-record → h264\n`.mp4`) with both the 26.6 and 27.0 toolchains. The physical-device path uses the same command\nand `-d` selector by design; it was not re-verified here against wired hardware.\n\n### Fallbacks (simulator-only)\n\nReach for these only when devicectl capture doesn't fit — none reach a physical device:\n\n| Tool | Use | Watch out |\n|------|-----|-----------|\n| `simctl io screenshot [--type png] ` | sim PNG; `-` writes to stdout | sim only |\n| `simctl io recordVideo [--codec h264\\|hevc] [--mask ignored\\|alpha\\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only |\n| `axe record-video --output f.mp4` / `axe stream-video` | sim video / live preview stream (mjpeg, jpeg, ffmpeg, bgra) | sim only; `record-video` stops on Ctrl+C — see `axiom-xcode-mcp (skills/axe-ref.md)` |\n\n## Status bar for screenshots\n\nA shipping screenshot needs a clean status bar — 9:41, full bars, full battery. On a simulator\nboth tools write the **same override store**: a `devicectl` override shows up in\n`simctl status_bar list`, and either tool's `clear` empties the other's view. Pick by toolchain,\nnot by effect.\n\n### Fastest path — the `screenshot` preset\n\n```bash\nSIM=$(xcrun simctl list devices booted | grep -Eo '[0-9A-F-]{36}' | head -1)\nxcrun devicectl device simulate statusBar preset -d \"$SIM\" screenshot\nxcrun devicectl device capture screenshot -d \"$SIM\" --destination shot.png\nxcrun devicectl device simulate statusBar clear -d \"$SIM\"\n```\n\n| Preset | Applies |\n|---|---|\n| `screenshot` | 9:41, battery 100 `charged`, cellular `active` 4 bars, `LTE`, Wi-Fi `active` 3 bars (verified readback) |\n| `low-battery` | 5% draining, weak signals |\n| `no-service` | no cellular or Wi-Fi |\n| `charging` | 50% charging, moderate signals |\n\n`screenshot` picks **LTE** (not 5G) and `charged` (a green charging battery, not a plain full\none). It rewrites every field it owns, so it is safe to re-run over a dirty state — unlike a bare\n`override`, below.\n\n**The preset also sets the carrier to the literal string `Carrier`** — the placeholder\n`screenshot-validator` flags as a defect. It is invisible on Dynamic Island iPhones, but renders on\niPad and older iPhones, and you **cannot** correct it afterward: `operatorName` refuses to overwrite\nan existing value (below). For any device that shows a carrier name, skip the preset and use the\n`simctl` path with an explicit `--operatorName` in the same `override` call.\n\n**`devicectl`'s subcommand set tracks a machine-wide component, not `xcode-select`.** Every\n`devicectl` binary defers to `/Library/Developer/PrivateFrameworks/CoreDevice.framework` — one copy\nper machine, upgraded by the newest Xcode installed — so selecting an older Xcode does *not* hand\nyou an older `devicectl`, and a second Xcode on the machine changes what the first one can do. Probe\nthe capability instead of inferring it from an Xcode version:\n\n```bash\nxcrun devicectl device simulate statusBar --help >/dev/null 2>&1 \\\n && echo \"statusBar available\" || echo \"fall back to simctl\"\n```\n\n### simctl — sim-only, and the only way to set a carrier name\n\n`devicectl` has no operator-name flag.\n\n```bash\nxcrun simctl status_bar \"$SIM\" clear # first — override merges\nxcrun simctl status_bar \"$SIM\" override \\\n --time \"9:41\" --dataNetwork lte --wifiMode active --wifiBars 3 \\\n --cellularMode active --cellularBars 4 --operatorName \"Your Co\" \\\n --batteryState charged --batteryLevel 100\n```\n\nThat is the preset's look with a real carrier name (verified readback). For a Wi-Fi-only status\nbar, drop **every** cellular flag — see the `wifi` trap below:\n\n```bash\nxcrun simctl status_bar \"$SIM\" override --time \"9:41\" \\\n --dataNetwork wifi --wifiMode active --wifiBars 3 --batteryState charged --batteryLevel 100\n```\n\n`--operatorName` only renders alongside a cellular override, and Dynamic Island iPhones show no\ncarrier name at all — set it for iPad and older-iPhone shots; don't debug its absence on an\niPhone 17.\n\n### Clear first; verify by readback, never by exit code\n\n`override` **merges** into whatever is already set, and some values silently fail to land. All of\nthese **exit 0**:\n\n| Silent failure | Stored result | Changes the capture? |\n|---|---|---|\n| `--dataNetwork wifi` alongside any `--cellularMode`, even `notSupported` | `5G` | No — the data label isn't drawn while Wi-Fi is active. Breaks readback assertions only |\n| `--dataNetwork wifi` over an existing data-network value | old value (`LTE`, `hide`) survives | Only on a Wi-Fi-only shot, where the label renders |\n| `--operatorName` over an existing carrier name | first value survives, including the preset's `Carrier` | Yes, on iPad and older iPhones |\n\nOther values overwrite normally (`LTE` replaces `wifi`, `hide` replaces `LTE`), so a script reads\nas working until the one case that changes pixels lands in a shipped screenshot.\n\n```bash\nxcrun devicectl device simulate statusBar clear -d \"$SIM\" # always, before override\nxcrun devicectl device simulate statusBar override -d \"$SIM\" --time \"9:41\" --battery-level 100\nxcrun devicectl device simulate statusBar show -d \"$SIM\" --json-output - # assert on this\n```\n\n`show` returns the full override set; `simctl status_bar list` prints the same state as\ntext. In CI, assert on the readback — a screenshot with a stale status bar is exactly the failure\nthis prevents, and no exit code reports it.\n\n### The two flag vocabularies are not copy-paste compatible\n\nCross-tool spellings are rejected outright: `simctl --batteryState draining` and\n`devicectl --battery-state discharging` both fail.\n\n| Field | devicectl | simctl |\n|---|---|---|\n| time | `--time \"9:41\"` | `--time \"9:41\"` |\n| data network | `--data-network` — `hide, wifi, hotspot, 1x, GPRS, Edge, UMTS, 4G, LTE, LTEA, LTEPlus, 5G, 5GPlus, 5GUWB, 5GUC` | `--dataNetwork` — `hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc` |\n| Wi-Fi | `--wifi-mode` (`notSupported, searching, failed, active`) + `--wifi-strength` | `--wifiMode` (`searching, failed, active`) + `--wifiBars` |\n| cellular | `--cellular-mode` + `--cellular-strength` | `--cellularMode` + `--cellularBars` |\n| battery | `--battery-state` (`draining, charging, charged`) + `--battery-level` | `--batteryState` (`discharging, charging, charged`) + `--batteryLevel` |\n| carrier | *(none)* | `--operatorName` |\n| not charging | `--battery-failing-to-charge` | *(none)* |\n\n`devicectl` matches its own values case-insensitively (`5g` sets `5G`); `simctl` does not\n(`--dataNetwork 5G` fails). Exit codes on a bad value: `devicectl` 1, `simctl` 117 for an unknown\nenum and 22 for an out-of-range integer.\n\n**`--cellular-strength`'s help text is wrong.** It advertises 1–5; a simulator rejects 5 with\n`Invalid value for cellular bars: expected 0-4`. Cap at 4, matching `simctl --cellularBars`.\n\n**Strip backticks when parsing `dataNetworkType` out of the JSON.** Values whose names aren't valid\nSwift identifiers come back wrapped — `` \"`5G`\" ``, `` \"`4G`\" ``, `` \"`1x`\" ``, `` \"`5GUC`\" `` —\nwhile `\"LTE\"`, `\"Edge\"`, `\"wifi\"`, and `\"hide\"` come back clean.\n\n### Overrides are cosmetic\n\ndevicectl states outright that these overrides \"do not affect the actual device state\": a `--time`\nof 9:41 changes the displayed string, not the clock, so a `Date()` read inside the app is\nunaffected. One exception on the simctl side — its `--time` help notes that a **valid ISO date\nstring** also sets the date on relevant devices.\n\nDevice Hub exposes no GUI control for status-bar overrides; run these commands while the simulator\nis displayed in Device Hub.\n\n## Resizable app sessions — `devicectl device appResize` `OS27`\n\nFree resize is **scriptable**. Device Hub's resize mode is the manual equivalent.\n\n> **Use `xcui resize sweep` for breakpoint testing.** It wraps everything below — session\n> lifecycle, actual-vs-requested readback, per-size assertions, and the 1001/24001/24004 error\n> split — in one command. See axiom-tools (skills/xcui-ref.md). Reach for the raw `devicectl`\n> calls here only when you need something the sweep does not cover. The commands are device-and-simulator shaped (`-d`\naccepts either), but only the **simulator** path was verified here — the physical-device path was\nnot re-verified against wired hardware.\n\n| Subcommand | Does |\n|---|---|\n| `appResize start -d [--preferred-size WxH] [--corner-radius R]` | Moves the frontmost apps to the Resizable display and holds the session. Runs until interrupted — **the session ends when the command exits**, so background it |\n| `appResize set -d --preferred-size WxH [--corner-radius R]` | Adjusts geometry of the live session from another terminal |\n| `appResize observe -d ` | Streams resizability state changes until interrupted |\n| `devicectl device info appResize -d ` | Current session state; fails with CoreDeviceError **24004** when no session is active |\n\nThe target display is auto-discovered by finding one whose name contains \"Resizable\".\n\n### Sweep breakpoints and assert each one\n\n```bash\nSIM=\nxcrun devicectl device appResize start -d $SIM --preferred-size 500x800 & # hold the session\nsleep 15\nfor SIZE in 900x600 1100x500 400x900; do\n xcrun devicectl device appResize set -d $SIM --preferred-size $SIZE\n sleep 5\n xcui assert --id primary-cta || { sleep 5; xcui assert --id primary-cta; }\ndone\nkill $! # ends the session\n```\n\n`xcui` and AXe attach normally during a session. Immediately after `start` the automation\nsession can time out once while the display transitions — retry rather than concluding the\ntools are blocked.\n\n**Read the actual size back; don't assume you got what you asked for.** Requesting `1100x500`\non an iPhone 17 simulator yielded `1100x550`. `start` prints `Requested`/`Actual` size and\ncorner radius plus `Minimum possible size` and `Maximum possible size` (`1280.0x1280.0` there),\nstreams `Scene state updated:` on every change, and `info appResize --json-output -` returns\n`preferredSize`, `cornerRadius`, `minimumPossibleSize`, `maximumPossibleSize`, and\n`displayUniqueId`.\n\n`--corner-radius` drives the container's corner radius, which makes it the way to exercise\nconcentric-corner behavior under CI — SwiftUI's `ConcentricRectangle` and UIKit's\n`UICornerRadius.containerConcentric(minimum:)`. Note SwiftUI has no `containerConcentric` corner\n*style*; that spelling is UIKit's and AppKit's. See axiom-swiftui (skills/26-ref.md).\n\n## Device Hub `OS27`\n\nXcode 27 unifies simulators and physical devices in **Device Hub** — a standalone app that ships\nalongside Xcode and auto-launches when you build and run to a simulator (you don't need to open\nXcode to use it), replacing the `Simulator.app` GUI. Xcode 26 and earlier keep `Simulator.app`, so\nit isn't \"gone\" for those users.\n\n**The bundle and process names changed, and scripts that force-quit the GUI depend on it:**\n\n| Xcode | Path | Process | Bundle id |\n|---|---|---|---|\n| 26 | `Contents/Developer/Applications/Simulator.app` | `Simulator` | `com.apple.iphonesimulator` |\n| 27 | `Contents/Applications/DeviceHub.app` | `DeviceHub` | `com.apple.dt.Devices` |\n\nXcode 27 ships **no** `Simulator.app` at any path, so `killall -9 Simulator` kills nothing there\n— it prints `No matching processes belonging to you were found` and leaves the stuck GUI running.\nName both: `killall -9 Simulator DeviceHub`.\n\n**Do not verify that with `$?`.** killall exits 0 when *either* name matched, so on a machine\ncarrying both Xcodes a 0 can mean \"killed Simulator, never touched DeviceHub\" — the GUI you were\ntrying to kill is still running. Confirm against the process, not the exit code:\n\n```bash\nkillall -9 Simulator DeviceHub\npgrep -l Simulator DeviceHub # must print NOTHING\n``` CarPlay simulation moved with it, into DeviceKit's\n`CarPlaySimulator.devicekitplugin`; the Xcode 26 `defaults write com.apple.iphonesimulator\nCarPlayExtraOptions -bool YES` key does not exist anywhere in the 27 toolchain.\n\nDevice Hub offers the same toolset for simulators and physical devices, in\na *compact* window (live screen plus a few essentials) that expands to a *full window* with canvas,\nsidebar inventory, and inspector. Bottom controls are contextual — home/screenshot/rotate on iPhone,\nplay/pause and navigation on Apple TV, environment/camera on Vision Pro, side button and Digital\nCrown on Apple Watch.\n\nThe **canvas** is a live, interactive screen (click, drag, scroll, trackpad gestures) with zoom,\nsnap-to-1:1 physical size, *Resize mode* (transform app dimensions freely — see `axiom-uikit` for\nresizability), and *Capture keyboard* (routes Mac keystrokes to the device for key-command and\nhardware testing).\n\n### Inspector panels\n\nFive panels; two carry most of the debugging weight — Diagnostic reports (investigate) and Device\nsettings (reproduce conditions).\n\n| Panel | Use |\n|---|---|\n| Device settings | Appearance and accessibility applied instantly — dark mode, increased contrast, larger Dynamic Type, simulated location, audio |\n| Diagnostic reports | Start here when the app hangs or crashes — crashes, spins, and other logged diagnostics |\n| Info | Storage, model, serial number |\n| Apps | Install/uninstall; download and replace data containers |\n| Profiles | Configuration and provisioning profiles |\n\nDevice Hub is a GUI over the same `devicectl`/`simctl` operations — a front-end, not a replacement.\nReach for the CLI in scripts, CI, and headless verification; for the reproduce-a-device-only-bug-on-a-\nsimulator debugging workflow, see `axiom-build (skills/xcode-debugging.md)`.\n\n## Resources\n\n**Skills**: xcui-ref, xclog-ref, axiom-build (xcode-debugging.md), axiom-testing (ui-testing.md), axiom-xcode-mcp\n", + "content": "# Device Control Reference — Device Hub, devicectl, simctl\n\nThe Xcode-independent surface for driving simulators and physical devices: which tool owns\nwhat, and what each one costs to run. Device Hub (the Xcode 27 GUI) is a front-end over\n`devicectl`/`simctl` — every operation has a scriptable, headless counterpart, so a full\ndev/CI loop needs no running Xcode.\n\n## Tool map — what each owns, and what it needs\n\n| Tool | Owns | Needs Xcode running? |\n|------|------|----------------------|\n| `devicectl` (CLI) | configure + interact with a booted sim OR physical device through one `-d ` selector; install/launch/inspect; capture screenshots + screen recordings; status-bar overrides incl. a `screenshot` preset; drive free resize (`appResize`) `OS27`; stable `--json-output` | No |\n| `simctl` (CLI) | simulator lifecycle (create/boot/shutdown/erase) + sim-only state: push, privacy permissions, media, `openurl`, `ui appearance`; shares the `status_bar` override store with devicectl and solely owns `--operatorName` | No |\n| `xcui` (Axiom) | drive in-app UI + accessibility tree (tap/assert, VoiceOver order); toggle a11y settings | No |\n| `xclog` (Axiom) | capture simulator/device console | No |\n| `xcsym` (Axiom) | symbolicate crashes (`.ips`, MetricKit, `.crash`) | No |\n| `xcprof` (Axiom) | record/analyze xctrace CPU & network profiles | No |\n| Device Hub (GUI) | visual front-end over devicectl/simctl — canvas, inspector; auto-launches on build-and-run | No (Xcode 27 installed, but needn't be open) |\n| `mcpbridge` (Xcode MCP) | IDE tools — build, test, render previews, project read | 26.x: **yes**. 27: no, via headless `mcp-server` (needs a `sudo` opt-in) `OS27` |\n\n**Answer to \"control the device without Xcode running\":** all of it.\n`devicectl` + `simctl` + Axiom's `xcui`/`xclog`/`xcsym`/`xcprof` cover the full scriptable\nsurface headlessly with no privilege escalation. On Xcode 27 the MCP bridge joins them —\n`sudo xcrun mcp-server enable` runs the tool service with Xcode.app closed\n(`axiom-xcode-mcp`) — so uptime is no longer what separates them `OS27`.\n\nPick on **capability and privilege** instead. `xcui` asserts, waits, toggles accessibility\nsettings, and computes VoiceOver announcements, and the whole CLI set runs unprivileged —\ndecisive in CI, where `sudo` is often unavailable. MCP owns what only the IDE knows: build\nstate and rendered previews. On Xcode 26.x the old rule still holds — `mcpbridge` alone\nneeds a running Xcode with a project open.\n\n## devicectl — the Core Device CLI\n\n`devicectl` (Xcode 15+, replaces the legacy `idevice*` tools) installs, launches, inspects, and\nconfigures devices from the command line. `xcrun devicectl list devices` returns a **unified\ninventory of physical devices *and* simulators**, distinguished by a `Reality` column\n(`physical` / `simulated`).\n\nThe pre-existing surface is materially identical across the 26 and 27 toolchains — same\nsubcommands and flags, verified against both (the exact binary build advances between beta seeds,\nso don't pin one). Xcode 27 adds: the `appResize` family (below), `devicectl device settings\nvoiceover`, and a service-side change letting `simctl` and `devicectl` reboot a simulator via\n`reboot`.\n\n```bash\n# Unified inventory: physical + simulated (--json-output for CI)\nxcrun devicectl list devices\n\n# Install / launch / inspect by identifier (sim UDID or device id — same -d)\nxcrun devicectl device install app --device MyApp.app\nxcrun devicectl device process launch --device com.your.bundleid\nxcrun devicectl device info apps --device \nxcrun devicectl device info processes --device \n```\n\n**Parse the structured `--json-output`, not the human-readable text.** devicectl guarantees the\nJSON is versioned and stable across releases; its human-readable output is explicitly *not* stable\n(simctl's human output never carried that guarantee either — the stability contract, not the\nunified `-d` syntax, is the real CI win).\n\n**Use the `properties` dictionary.** The older top-level `hardwareProperties`, `deviceProperties`,\nand `connectionProperties` keys are deprecated in favor of one `properties` dictionary and will be\nremoved; the `tags` key is already gone. Pass `--omit-deprecated-fields-in-json` to drop them now\nand prove a parser is forward-compatible. The fields worth keying off:\n\n| Field | Values | Always present |\n|---|---|---|\n| `properties.hardware.reality` | `physical` / `simulated` | Yes |\n| `properties.hardware.deviceType` | `iPhone`, `iPad`, `appleWatch`, … | Yes |\n| `properties.hardware.platform` | `iOS`, `watchOS`, … | Yes |\n| `properties.connection.state` | `connected` / `disconnected` / `unavailable` | Yes |\n| `properties.connection.pairingState` | `paired`, … | Yes |\n| `properties.state.developerModeStatus` | single-key dict: `{\"enabled\":{}}` / `{\"disabled\":{}}` | Physical only |\n| `properties.connection.transportType` | `sameMachine` (sim) / `localNetwork` / `wired` | **No** |\n| `properties.state.bootState` | `booted` / `shutdown` | **No** |\n\n**Read `reality` directly; never derive it from `transportType`.** `transportType` and `bootState`\nare absent on a physical device with no active connection — precisely the device you are debugging.\nA parser that infers physical-vs-simulated from `transportType` misclassifies or throws on exactly\nthe rows that matter. `hardware.reality` is a literal field and is always populated.\n\n`developerModeStatus` encodes an enum as a single-key dictionary, so test key presence\n(`\"enabled\" in status`), not a string compare. `connection.lastConnectionDate` is a number in this\nschema (seconds from the 2001 epoch) where the deprecated keys carried an ISO-8601 string — a\nsilent type change to catch when migrating.\n\nFor reading these states on an Apple Watch that will not appear or will not run, see\naxiom-watchos (`skills/watch-device-diag.md`).\n\n### Interaction vs lifecycle — devicectl does NOT replace simctl\n\ndevicectl **configures and interacts** with a booted device/sim; it has no `create`/`boot`/`erase`.\nsimctl still owns the simulator lifecycle and the sim-only features.\n\n| Need | Tool |\n|------|------|\n| create / boot / shutdown / erase a sim | `xcrun simctl boot\\|shutdown\\|erase` |\n| pick the test destination | `xcodebuild -destination` |\n| configure / interact with a booted sim or device | `xcrun devicectl` |\n| push, privacy permissions, media, openurl | `xcrun simctl` (sim-only) |\n| status bar overrides for screenshots | either — same store; `devicectl` adds a `screenshot` preset, `simctl` owns `--operatorName` |\n\nCI order is unchanged at the front: simctl or xcodebuild boots the sim → devicectl configures it\n→ run tests.\n\n### Simulator-capable subcommands (verified on Xcode 26.6 + 27.0)\n\n| Subcommand | On simulator | Use |\n|------------|--------------|-----|\n| `device info displays` | works (verified) | bounds, pointScale, nativeSize, `framebufferMaskIdentifier` (exact JSON keys) |\n| `device capture screenshot` / `screen-record` | works (verified) | PNG / H.264 `.mp4` capture, sim or device — see Screen capture below |\n| `device orientation set` / `get` (also `rotate`) | works (verified) — **except iPhone Duo**, see Rotating a simulator | rotate a sim headlessly; simctl has no rotate verb |\n| `device motion hinge-angle` | works, read-only | streams the hinge angle (0° closed, 180° open) for 60 s by default; nothing in devicectl or simctl sets it |\n| `device settings biometrics [--enable\\|--disable]` | works (verified) | enroll / unenroll Face ID / Touch ID |\n| `device simulate biometrics --success\\|--failure` | works (verified) | drive a match / no-match |\n| `device settings appearance --mode light\\|dark` | works (verified) | force Dark/Light; also `--look-and-feel clear\\|tinted`, text size, contrast |\n| `device settings voiceover --enable\\|--disable` (also `device info voiceover`) | works (verified) `OS27` | toggle VoiceOver — the one a11y toggle `xcui` omitted for lack of a mechanism |\n| `device simulate location` | available | inject location |\n| `device simulate statusBar` (`preset`/`override`/`show`/`clear`) | works (verified) | clean status bar for screenshots — see Status bar for screenshots below |\n| `device process sendMemoryWarning` | available | memory-pressure scenarios |\n| `device info lockState` / `info files` / `copy` / `profile *` | physical-device-only | see caveat below |\n\n**Face ID / Touch ID is devicectl-only** — simctl has no biometric command (enrolling/matching was\na GUI-only Simulator menu, unscriptable):\n\n```bash\nSIM=$(xcrun simctl list devices booted | grep -Eo '[0-9A-F-]{36}' | head -1)\nxcrun devicectl device settings biometrics -d \"$SIM\" --enable # enroll\nxcrun devicectl device simulate biometrics -d \"$SIM\" --success # match (--failure for the reject path)\nxcrun devicectl device settings biometrics -d \"$SIM\" --disable # restore\n```\n\nThe flags are `--success` / `--failure` (mutually exclusive) — **not** `--match`.\n\n**Physical-device-only capabilities** on a simulator fail with a distinct, detectable error — not a\ncrash, not a silent no-op:\n\n```\nERROR: The capability \"Get Lock State\" is not supported by this device.\n (com.apple.dt.CoreDeviceError error 1001)\n```\n\n`info lockState` is confirmed device-only; `info files`, `copy`, and `profile *` are reported\ndevice-only on simulators. In CI, treat `CoreDeviceError 1001` as \"skip on simulator\", not a failure.\n\n### Rotating a simulator\n\n`simctl` has no rotate verb, and keystroke automation of the GUI is no substitute — it reported success while rotating nothing. Rotate from the host with devicectl and verify by screenshot **dimensions**:\n\n```bash\nxcrun devicectl device orientation set -d \"$SIM\" landscapeLeft # portrait | portraitUpsideDown | landscapeLeft | landscapeRight\nxcrun simctl io \"$SIM\" screenshot rotated.png # width and height must swap\nxcrun devicectl device orientation set -d \"$SIM\" portrait\n```\n\nVerified 2026-09-19: iPhone 17 (iOS 27.0) 1206×2622 → 2622×1206, iPad Pro 11-inch (iOS 27.0) 1668×2420 → 2420×1668, and the app received the new size both times.\n\n- **iPhone Duo ignores it.** `set` prints `New Device Orientation: landscapeLeft`, `get` still reads `portrait`, and nothing rotates — measured closed, and reported open on the inner display too. Duo poses change only through Device Hub's pose controls: axiom-swiftui (skills/iphone-duo.md, Tooling and Testing).\n- **On iPad, only the host can rotate.** An app's own `requestGeometryUpdate` is refused on iPad under the windowing model (`UISceneErrorDomain` 101; axiom-uikit (skills/uikit-modernization.md)), so a test that needs landscape should rotate from the harness with the command above, not from inside the app.\n\n## Screen capture — screenshot & video\n\n`devicectl device capture` is the **unified** capture path: one `-d ` selector across\nsimulators and physical devices, the same stable `--json-output`, and — for video — a\n`--duration` auto-stop that makes it the only script/CI-friendly recorder of the options here.\nPresent and verified on **both Xcode 26.6 and 27.0** (not new in 27 — another instance of the\n\"materially identical across 26 and 27\" CLI).\n\n```bash\n# Screenshot — destination MUST end in .png\nxcrun devicectl device capture screenshot -d --destination shot.png\n\n# Screen recording — destination MUST end in .mp4; --duration auto-stops (else Ctrl+C)\nxcrun devicectl device capture screen-record -d --destination clip.mp4 --duration 5\n```\n\n| Flag | screenshot | screen-record | Notes |\n|------|------------|---------------|-------|\n| `--destination` | `.png` only | `.mp4` only | wrong extension is a hard error, not a coercion |\n| `--display-unique-id` | yes | yes | the `uniqueId` from `device info displays`. Omitted, the capture is **not** always the primary display — on iPhone Duo it is the inner display, even while closed and dark |\n| `--codec` | — | `h264` (default), `hevc` | |\n| `--mask-policy` | — | `ignored` (default), `premultipliedAlpha`, `black` | bezel mask for non-rectangular displays |\n| `--duration ` | — | auto-stop after N seconds | omit = record until SIGINT |\n\nVerified on a booted iOS 26.5 simulator (screenshot → 1206×2622 PNG; screen-record → h264\n`.mp4`) with both the 26.6 and 27.0 toolchains. The physical-device path uses the same command\nand `-d` selector by design; it was not re-verified here against wired hardware.\n\n### Fallbacks (simulator-only)\n\nReach for these only when devicectl capture doesn't fit — none reach a physical device:\n\n| Tool | Use | Watch out |\n|------|-----|-----------|\n| `simctl io screenshot [--type png] [--display ] [--mask ignored\\|alpha\\|black] ` | sim PNG; `-` writes to stdout | sim only; `--display` takes a port UUID from `simctl io enumerate` (not devicectl's `uniqueId`) |\n| `simctl io recordVideo [--codec h264\\|hevc] [--mask ignored\\|black] ` | sim video to a `.mov` | default codec is `hevc` (devicectl defaults `h264`); stop with SIGINT; sim only. `--mask alpha` is accepted but unsupported for video — it renders black |\n| `axe record-video --output f.mp4` / `axe stream-video` | sim video / live preview stream (mjpeg, jpeg, ffmpeg, bgra) | sim only; `record-video` stops on Ctrl+C — see `axiom-xcode-mcp (skills/axe-ref.md)` |\n\n### Display masks and multi-display devices\n\nFor non-rectangular displays, `simctl io … screenshot --mask` chooses what the corners hold:\n\n| `--mask` | Corners | Use it to see |\n|---|---|---|\n| `alpha` | transparent (premultiplied alpha) | what the user sees on the glass |\n| `ignored` | the full framebuffer rectangle | what a device screenshot captures — anything a view leaves under a corner lands in it |\n| `black` | black, no alpha channel | a flat image for tools that drop alpha |\n\nCompare `alpha` and `ignored` when judging content near a corner. The alpha channel is also a measurement: on iPhone Duo's outer display the fully transparent run along the top and bottom rows is 18 px at the left corners and 185 px at the right (iOS 27.1 simulator).\n\n**A device with two displays needs the display named.** iPhone Duo's default capture — `simctl io` and `devicectl device capture` alike — is the inner display (2007×2853 px), which is black while the device is closed. Capture the outer display (1398×2034 px) explicitly:\n\n```bash\nxcrun simctl io \"$SIM\" enumerate # the outer display's port is the one 1398 wide\nxcrun simctl io \"$SIM\" screenshot --display= outer.png\nxcrun devicectl device info displays -d \"$SIM\" # outer is \"LCD\", inner \"LCD-1\"; shows which is active\nxcrun devicectl device capture screenshot -d \"$SIM\" --display-unique-id --destination outer.png\n```\n\nThe two tools use different identifiers for the same display: simctl wants the **port UUID** from `enumerate`, devicectl the **`uniqueId`** from `info displays`. Guessing names fails — simctl rejects `--display=LCD-2`, and `2`, `4`, and `external` are accepted but hang without a frame.\n\n## Status bar for screenshots\n\nA shipping screenshot needs a clean status bar — 9:41, full bars, full battery. On a simulator\nboth tools write the **same override store**: a `devicectl` override shows up in\n`simctl status_bar list`, and either tool's `clear` empties the other's view. Pick by toolchain,\nnot by effect.\n\n### Fastest path — the `screenshot` preset\n\n```bash\nSIM=$(xcrun simctl list devices booted | grep -Eo '[0-9A-F-]{36}' | head -1)\nxcrun devicectl device simulate statusBar preset -d \"$SIM\" screenshot\nxcrun devicectl device capture screenshot -d \"$SIM\" --destination shot.png\nxcrun devicectl device simulate statusBar clear -d \"$SIM\"\n```\n\n| Preset | Applies |\n|---|---|\n| `screenshot` | 9:41, battery 100 `charged`, cellular `active` 4 bars, `LTE`, Wi-Fi `active` 3 bars (verified readback) |\n| `low-battery` | 5% draining, weak signals |\n| `no-service` | no cellular or Wi-Fi |\n| `charging` | 50% charging, moderate signals |\n\n`screenshot` picks **LTE** (not 5G) and `charged` (a green charging battery, not a plain full\none). It rewrites every field it owns, so it is safe to re-run over a dirty state — unlike a bare\n`override`, below.\n\n**The preset also sets the carrier to the literal string `Carrier`** — the placeholder\n`screenshot-validator` flags as a defect. It is invisible on Dynamic Island iPhones, but renders on\niPad and older iPhones, and you **cannot** correct it afterward: `operatorName` refuses to overwrite\nan existing value (below). For any device that shows a carrier name, skip the preset and use the\n`simctl` path with an explicit `--operatorName` in the same `override` call.\n\n**`devicectl`'s subcommand set tracks a machine-wide component, not `xcode-select`.** Every\n`devicectl` binary defers to `/Library/Developer/PrivateFrameworks/CoreDevice.framework` — one copy\nper machine, upgraded by the newest Xcode installed — so selecting an older Xcode does *not* hand\nyou an older `devicectl`, and a second Xcode on the machine changes what the first one can do. Probe\nthe capability instead of inferring it from an Xcode version:\n\n```bash\nxcrun devicectl device simulate statusBar --help >/dev/null 2>&1 \\\n && echo \"statusBar available\" || echo \"fall back to simctl\"\n```\n\n### simctl — sim-only, and the only way to set a carrier name\n\n`devicectl` has no operator-name flag.\n\n```bash\nxcrun simctl status_bar \"$SIM\" clear # first — override merges\nxcrun simctl status_bar \"$SIM\" override \\\n --time \"9:41\" --dataNetwork lte --wifiMode active --wifiBars 3 \\\n --cellularMode active --cellularBars 4 --operatorName \"Your Co\" \\\n --batteryState charged --batteryLevel 100\n```\n\nThat is the preset's look with a real carrier name (verified readback). For a Wi-Fi-only status\nbar, drop **every** cellular flag — see the `wifi` trap below:\n\n```bash\nxcrun simctl status_bar \"$SIM\" override --time \"9:41\" \\\n --dataNetwork wifi --wifiMode active --wifiBars 3 --batteryState charged --batteryLevel 100\n```\n\n`--operatorName` only renders alongside a cellular override, and Dynamic Island iPhones show no\ncarrier name at all — set it for iPad and older-iPhone shots; don't debug its absence on an\niPhone 17.\n\n### Clear first; verify by readback, never by exit code\n\n`override` **merges** into whatever is already set, and some values silently fail to land. All of\nthese **exit 0**:\n\n| Silent failure | Stored result | Changes the capture? |\n|---|---|---|\n| `--dataNetwork wifi` alongside any `--cellularMode`, even `notSupported` | `5G` | No — the data label isn't drawn while Wi-Fi is active. Breaks readback assertions only |\n| `--dataNetwork wifi` over an existing data-network value | old value (`LTE`, `hide`) survives | Only on a Wi-Fi-only shot, where the label renders |\n| `--operatorName` over an existing carrier name | first value survives, including the preset's `Carrier` | Yes, on iPad and older iPhones |\n\nOther values overwrite normally (`LTE` replaces `wifi`, `hide` replaces `LTE`), so a script reads\nas working until the one case that changes pixels lands in a shipped screenshot.\n\n```bash\nxcrun devicectl device simulate statusBar clear -d \"$SIM\" # always, before override\nxcrun devicectl device simulate statusBar override -d \"$SIM\" --time \"9:41\" --battery-level 100\nxcrun devicectl device simulate statusBar show -d \"$SIM\" --json-output - # assert on this\n```\n\n`show` returns the full override set; `simctl status_bar list` prints the same state as\ntext. In CI, assert on the readback — a screenshot with a stale status bar is exactly the failure\nthis prevents, and no exit code reports it.\n\n### The two flag vocabularies are not copy-paste compatible\n\nCross-tool spellings are rejected outright: `simctl --batteryState draining` and\n`devicectl --battery-state discharging` both fail.\n\n| Field | devicectl | simctl |\n|---|---|---|\n| time | `--time \"9:41\"` | `--time \"9:41\"` |\n| data network | `--data-network` — `hide, wifi, hotspot, 1x, GPRS, Edge, UMTS, 4G, LTE, LTEA, LTEPlus, 5G, 5GPlus, 5GUWB, 5GUC` | `--dataNetwork` — `hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc` |\n| Wi-Fi | `--wifi-mode` (`notSupported, searching, failed, active`) + `--wifi-strength` | `--wifiMode` (`searching, failed, active`) + `--wifiBars` |\n| cellular | `--cellular-mode` + `--cellular-strength` | `--cellularMode` + `--cellularBars` |\n| battery | `--battery-state` (`draining, charging, charged`) + `--battery-level` | `--batteryState` (`discharging, charging, charged`) + `--batteryLevel` |\n| carrier | *(none)* | `--operatorName` |\n| not charging | `--battery-failing-to-charge` | *(none)* |\n\n`devicectl` matches its own values case-insensitively (`5g` sets `5G`); `simctl` does not\n(`--dataNetwork 5G` fails). Exit codes on a bad value: `devicectl` 1, `simctl` 117 for an unknown\nenum and 22 for an out-of-range integer.\n\n**`--cellular-strength`'s help text is wrong.** It advertises 1–5; a simulator rejects 5 with\n`Invalid value for cellular bars: expected 0-4`. Cap at 4, matching `simctl --cellularBars`.\n\n**Strip backticks when parsing `dataNetworkType` out of the JSON.** Values whose names aren't valid\nSwift identifiers come back wrapped — `` \"`5G`\" ``, `` \"`4G`\" ``, `` \"`1x`\" ``, `` \"`5GUC`\" `` —\nwhile `\"LTE\"`, `\"Edge\"`, `\"wifi\"`, and `\"hide\"` come back clean.\n\n### Overrides are cosmetic\n\ndevicectl states outright that these overrides \"do not affect the actual device state\": a `--time`\nof 9:41 changes the displayed string, not the clock, so a `Date()` read inside the app is\nunaffected. One exception on the simctl side — its `--time` help notes that a **valid ISO date\nstring** also sets the date on relevant devices.\n\nDevice Hub exposes no GUI control for status-bar overrides; run these commands while the simulator\nis displayed in Device Hub.\n\n## Resizable app sessions — `devicectl device appResize` `OS27`\n\nFree resize is **scriptable**. Device Hub's resize mode is the manual equivalent.\n\n> **Use `xcui resize sweep` for breakpoint testing.** It wraps everything below — session\n> lifecycle, actual-vs-requested readback, per-size assertions, and the 1001/24001/24004 error\n> split — in one command. See axiom-tools (skills/xcui-ref.md). Reach for the raw `devicectl`\n> calls here only when you need something the sweep does not cover. The commands are device-and-simulator shaped (`-d`\naccepts either), but only the **simulator** path was verified here — the physical-device path was\nnot re-verified against wired hardware.\n\n| Subcommand | Does |\n|---|---|\n| `appResize start -d [--preferred-size WxH] [--corner-radius R]` | Moves the frontmost apps to the Resizable display and holds the session. Runs until interrupted — **the session ends when the command exits**, so background it |\n| `appResize set -d --preferred-size WxH [--corner-radius R]` | Adjusts geometry of the live session from another terminal |\n| `appResize observe -d ` | Streams resizability state changes until interrupted |\n| `devicectl device info appResize -d ` | Current session state; fails with CoreDeviceError **24004** when no session is active |\n\nThe target display is auto-discovered by finding one whose name contains \"Resizable\".\n\n### Sweep breakpoints and assert each one\n\n```bash\nSIM=\nxcrun devicectl device appResize start -d $SIM --preferred-size 500x800 & # hold the session\nsleep 15\nfor SIZE in 900x600 1100x500 400x900; do\n xcrun devicectl device appResize set -d $SIM --preferred-size $SIZE\n sleep 5\n xcui assert --id primary-cta || { sleep 5; xcui assert --id primary-cta; }\ndone\nkill $! # ends the session\n```\n\n`xcui` and AXe attach normally during a session. Immediately after `start` the automation\nsession can time out once while the display transitions — retry rather than concluding the\ntools are blocked.\n\n**Read the actual size back; don't assume you got what you asked for.** Requesting `1100x500`\non an iPhone 17 simulator yielded `1100x550`. `start` prints `Requested`/`Actual` size and\ncorner radius plus `Minimum possible size` and `Maximum possible size` (`1280.0x1280.0` there),\nstreams `Scene state updated:` on every change, and `info appResize --json-output -` returns\n`preferredSize`, `cornerRadius`, `minimumPossibleSize`, `maximumPossibleSize`, and\n`displayUniqueId`.\n\n`--corner-radius` drives the container's corner radius, which makes it the way to exercise\nconcentric-corner behavior under CI — SwiftUI's `ConcentricRectangle` and UIKit's\n`UICornerRadius.containerConcentric(minimum:)`. Note SwiftUI has no `containerConcentric` corner\n*style*; that spelling is UIKit's and AppKit's. See axiom-swiftui (skills/26-ref.md).\n\n## Device Hub `OS27`\n\nXcode 27 unifies simulators and physical devices in **Device Hub** — a standalone app that ships\nalongside Xcode and auto-launches when you build and run to a simulator (you don't need to open\nXcode to use it), replacing the `Simulator.app` GUI. Xcode 26 and earlier keep `Simulator.app`, so\nit isn't \"gone\" for those users.\n\n**The bundle and process names changed, and scripts that force-quit the GUI depend on it:**\n\n| Xcode | Path | Process | Bundle id |\n|---|---|---|---|\n| 26 | `Contents/Developer/Applications/Simulator.app` | `Simulator` | `com.apple.iphonesimulator` |\n| 27 | `Contents/Applications/DeviceHub.app` | `DeviceHub` | `com.apple.dt.Devices` |\n\nXcode 27 ships **no** `Simulator.app` at any path, so `killall -9 Simulator` kills nothing there\n— it prints `No matching processes belonging to you were found` and leaves the stuck GUI running.\nName both: `killall -9 Simulator DeviceHub`.\n\n**Do not verify that with `$?`.** killall exits 0 when *either* name matched, so on a machine\ncarrying both Xcodes a 0 can mean \"killed Simulator, never touched DeviceHub\" — the GUI you were\ntrying to kill is still running. Confirm against the process, not the exit code:\n\n```bash\nkillall -9 Simulator DeviceHub\npgrep -l Simulator DeviceHub # must print NOTHING\n```\n\nCarPlay simulation moved with it, into DeviceKit's\n`CarPlaySimulator.devicekitplugin`; the Xcode 26 `defaults write com.apple.iphonesimulator\nCarPlayExtraOptions -bool YES` key does not exist anywhere in the 27 toolchain.\n\nDevice Hub offers the same toolset for simulators and physical devices, in\na *compact* window (live screen plus a few essentials) that expands to a *full window* with canvas,\nsidebar inventory, and inspector. Bottom controls are contextual — home/screenshot/rotate on iPhone,\nplay/pause and navigation on Apple TV, environment/camera on Vision Pro, side button and Digital\nCrown on Apple Watch.\n\nThe **canvas** is a live, interactive screen (click, drag, scroll, trackpad gestures) with zoom,\nsnap-to-1:1 physical size, *Resize mode* (transform app dimensions freely — see `axiom-uikit` for\nresizability), and *Capture keyboard* (routes Mac keystrokes to the device for key-command and\nhardware testing).\n\n### Inspector panels\n\nFive panels; two carry most of the debugging weight — Diagnostic reports (investigate) and Device\nsettings (reproduce conditions).\n\n| Panel | Use |\n|---|---|\n| Device settings | Appearance and accessibility applied instantly — dark mode, increased contrast, larger Dynamic Type, simulated location, audio |\n| Diagnostic reports | Start here when the app hangs or crashes — crashes, spins, and other logged diagnostics |\n| Info | Storage, model, serial number |\n| Apps | Install/uninstall; download and replace data containers |\n| Profiles | Configuration and provisioning profiles |\n\nDevice Hub is a GUI over the same `devicectl`/`simctl` operations — a front-end, not a replacement.\nReach for the CLI in scripts, CI, and headless verification; for the reproduce-a-device-only-bug-on-a-\nsimulator debugging workflow, see `axiom-build (skills/xcode-debugging.md)`.\n\n## Resources\n\n**Skills**: xcui-ref, xclog-ref, axiom-build (xcode-debugging.md), axiom-testing (ui-testing.md), axiom-xcode-mcp\n", "skillType": "reference", "source": "axiom", "tags": [ @@ -40917,56 +40917,56 @@ "heading": "Install / launch / inspect by identifier (sim UDID or device id — same -d)", "level": 1, "startLine": 49, - "endLine": 144, - "charCount": 5856 + "endLine": 160, + "charCount": 7388 }, { "heading": "Screen capture — screenshot & video", "level": 2, - "startLine": 145, - "endLine": 153, + "startLine": 161, + "endLine": 169, "charCount": 461 }, { "heading": "Screenshot — destination MUST end in .png", "level": 1, - "startLine": 154, - "endLine": 156, + "startLine": 170, + "endLine": 172, "charCount": 119 }, { "heading": "Screen recording — destination MUST end in .mp4; --duration auto-stops (else Ctrl+C)", "level": 1, - "startLine": 157, - "endLine": 182, - "charCount": 1666 + "startLine": 173, + "endLine": 221, + "charCount": 3700 }, { "heading": "Status bar for screenshots", "level": 2, - "startLine": 183, - "endLine": 310, + "startLine": 222, + "endLine": 349, "charCount": 6757 }, { "heading": "Resizable app sessions — `devicectl device appResize` `OS27`", "level": 2, - "startLine": 311, - "endLine": 360, + "startLine": 350, + "endLine": 399, "charCount": 2859 }, { "heading": "Device Hub `OS27`", "level": 2, - "startLine": 361, - "endLine": 417, - "charCount": 3111 + "startLine": 400, + "endLine": 458, + "charCount": 3112 }, { "heading": "Resources", "level": 2, - "startLine": 418, - "endLine": 421, + "startLine": 459, + "endLine": 462, "charCount": 128 } ], @@ -41571,8 +41571,8 @@ }, "axiom-tools--xcui-ref": { "name": "axiom-tools--xcui-ref", - "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself.", - "content": "# xcui Reference (Scriptable Simulator UI & Accessibility Testing)\n\nxcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself.\n\n## Invocation\n\nOn **Claude Code**, `xcui` is already on PATH — the plugin's `bin/` is auto-resolved. Run `xcui `.\n\nOn **Codex, Pi, and MCP installs there is no bundled binary**: those install skills only, and no install path puts Axiom's tools on `PATH`. Check with `command -v xcui` before using any `xcui` command in this file.\n\nIf it is absent, you have two working options and should say which you took:\n- Put it on PATH — clone the repo and symlink `.claude-plugin/plugins/axiom/bin/xcui` (see the Pi install guide). This is the only way to get `resize sweep`, `wait`, `assert`, `a11y`, `dialog`, and `voiceover`, which have no AXe equivalent.\n- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards, and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided.\n\n## Prerequisite: run `xcui doctor`\n\n`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready; exit 2 = AXe missing or no booted sim (see `problems`/`next_steps` in the JSON). When several sims are booted, every verb targets the lowest UDID deterministically; `doctor` adds a `note` listing them, and `--udid ` (accepted by every verb, `doctor` included) targets a specific one.\n\n## Subcommands\n\n- `xcui wait --for-element | --gone | --idle [--timeout 10s] [--poll 250ms]` — poll the a11y tree until a condition holds. Replaces sleep/re-screenshot guesswork (CLI `waitForExistence`).\n- `xcui assert --id [--label ] [--value ] [--trait ] [--single]` — assert on an element. `--single` checks the id resolves to exactly one element (e.g. \"hero announces as one element\").\n- `xcui a11y set --toggle --value [--app ]` — set an accessibility setting. Supported toggles (all verified against the simulator):\n - `dynamic-type` — native `simctl ui content_size`; `--value` is a size (`large`, `accessibility-extra-large`, … up to `accessibility-extra-extra-extra-large`). Applies live; no relaunch.\n - `increase-contrast` — native `simctl ui increase_contrast`; `--value` is `on`/`off`. Applies live; no relaunch.\n - `reduce-motion` — `defaults write com.apple.Accessibility ReduceMotionEnabled`; needs relaunch, so pass `--app ` to have xcui terminate + relaunch the app.\n - `reduce-transparency` — `defaults write com.apple.Accessibility ReduceTransparencyEnabled`; needs relaunch (pass `--app`).\n - `voiceover` — `devicectl device settings voiceover --enable|--disable`; `--value` is `on`/`off`. Applies live; no relaunch. The only toggle that leaves the simctl/defaults world — simctl has no VoiceOver setter. Read the state back with `xcrun devicectl device info voiceover -d `.\n- `xcui a11y reset` — clear xcui-set overrides (delete the defaults keys, content_size → large, increase_contrast → disabled).\n- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert.\n- `xcui dialog pregrant … [--udid ]` — grant permissions ahead of time via `simctl privacy … grant`, so the dialog never appears. Services are `simctl privacy` names (`camera`, `photos`, `location`, `microphone`, `contacts`, …). Prefer this over `accept` when you control the test setup — no alert means nothing to race.\n- `xcui voiceover traverse [--udid ]` — emit the **computed** VoiceOver announcement sequence: walk the a11y tree in focus order (top-to-bottom, leading-to-trailing) and render each focusable element as `label, value, trait` (plus `dimmed` when disabled). Output is a `sequence` JSON array.\n- `xcui voiceover assert --sequence [--udid ]` — compare the live announcement sequence to an expected one; the file may be a bare JSON string array **or** a saved `traverse` report (it round-trips). Reports every differing index (one entry per mismatched position, plus a length-mismatch note when counts differ); exit `1` on any mismatch.\n\n> **VoiceOver scope (honest framing):** `voiceover` renders the *computed* announcement from the accessibility tree — what VoiceOver would say, derived deterministically. It is **not** captured audio/TTS, which the simulator does not expose to scripting. Use it to catch missing labels, wrong trait phrasing, bad focus order, and unannounced state — not to verify the speech synthesizer itself.\n\n> **Turning VoiceOver ON is not the same as `xcui voiceover`.** Two different things, easy to conflate:\n> - `xcui a11y set --toggle voiceover --value on` — actually starts the screen reader on the device.\n> - `xcui voiceover traverse` / `assert` — **computes** the announcement sequence from the accessibility tree, with VoiceOver *off*. This is the one you want in CI: deterministic, no speech, no focus stealing.\n>\n> You do not need the toggle to run `traverse`. Reach for the toggle when you need the device in a real VoiceOver state — verifying that your app behaves correctly while a screen reader is actually running.\n\n> **Still not supported (a11y toggles):** `differentiate-without-color` and `bold-text` have no confirmable simulator mechanism — no native `simctl ui` setter, candidate `defaults` keys not honored by iOS on the sim, and `devicectl device settings` offers only appearance, audio, biometrics, reset, and voiceover. Omitted rather than shipped unverified.\n\n## Resize sweeps `OS27`\n\n`xcui resize sweep --sizes 400x900,900x600,1100x500 [--assert-id ] [--corner-radius R] [--settle 3s]` drives a resizable-app session across breakpoints and asserts at each one, in a single JSON envelope. It wraps the three tedious parts of `devicectl device appResize`:\n\n- **Session lifecycle.** `appResize start` holds the session only while it runs, so it must be backgrounded and killed. xcui owns that; interrupting a sweep does not strand the app on the Resizable display.\n- **Actual vs requested size.** xcui reports both and flags `honored: false` when they differ. Some devices clamp — an iPhone 17 turned a requested `1100x500` into `1100x550`. A sweep that trusted the request would report a clean pass for a width it never exercised.\n- **Post-resize flakiness.** The automation session can time out once while the display transitions, so each assertion retries before failing.\n\n`--screenshot-dir ` writes `x.png` per breakpoint, captured from the Resizable display. **Reach for this before `--assert-id`**: an id-presence check catches \"the element vanished\", but resizing breaks layouts by overlap, truncation, and clipping — which only a picture shows.\n\n`--strict` fails any step the device clamped. Without it a clamped sweep still exits `0` when the assertions pass, so an exit-code-only consumer reads \"all breakpoints validated\" for breakpoints that were never produced; the `clamped` count and a `note` say so either way.\n\nExit `0` all sizes passed · `1` an assertion, screenshot, or (under `--strict`) a clamp failed · `2` environment error.\n\nThree `devicectl` failures are separated, because they need different fixes:\n\n| CoreDeviceError | Means | Fix |\n|---|---|---|\n| 1001 | device has no Resizable App Management | use an OS 27 simulator or device |\n| 24001 | nothing in the foreground to move | launch the app first |\n| 24004 | no session active | not an error during startup — xcui polls through it |\n\n## Device state — biometrics, orientation, location: use `devicectl`\n\n`xcui` drives the **in-app UI + accessibility tree** (`tap`/`assert`, VoiceOver order) and toggles **accessibility settings** (`a11y set`: Dynamic Type, contrast, motion). It does **not** drive hardware/device state. For **biometrics** (Face ID / Touch ID — neither `xcui` nor `simctl` can do this), orientation, location, status bar, and memory-pressure, use `devicectl`: it works on simulators in Xcode 26.6+, takes one `-d ` selector across sim + device, and emits a stable `--json-output`. The two compose — `devicectl` sets the state, `xcui` asserts the resulting UI. Full verified catalog: `skills/device-control-ref.md`.\n\n## Input — `xcui tap`, not `axe tap`\n\nInput verbs forward to AXe verbatim: same flags, same output, same exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling comes along automatically, so guidance can't drift out of it.\n\nForwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted. `xcui tap --help` shows AXe's own flags.\n\n```bash\nxcui tap --id loginButton --udid # real HID touch, not pointer-hover\nxcui type \"user@example.com\" --udid \naxe describe-ui --udid # raw a11y tree (xcui assert/wait parse this)\n```\n\n> **Still calling `axe` directly?** `describe-ui`, `stream-video`, and `record-video` stay bare — the first is what xcui itself parses, and the two streaming verbs outlive any request timeout. Those are the only calls that need a `DEVELOPER_DIR=` prefix, and only when `xcui doctor` reports an `axe_developer_dir` (rare — AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided).\n\n## Output & exit codes\n\nJSON by default (`tool`/`version` envelope); `--human` for prose. Exit: `0` pass · `1` assertion-fail/wait-timeout · `2` environment error · `8` output-write error.\n\n> **CLI gotcha:** Go's flag parser stops at the first positional, so always put flags after the subcommand and use the all-flag forms shown above (`assert --id …`, not `assert …`).\n\n## Resources\n\n**Tools**: `axe` (AXe — `brew install cameroncooke/axe/axe`), `xcrun simctl`\n\n**Skills**: axiom-accessibility, axiom-testing\n\n**Agents**: simulator-tester (drives xcui live), accessibility-auditor (static a11y scan)\n", + "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself.", + "content": "# xcui Reference (Scriptable Simulator UI & Accessibility Testing)\n\nxcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself.\n\n## Invocation\n\nOn **Claude Code**, `xcui` is already on PATH — the plugin's `bin/` is auto-resolved. Run `xcui `.\n\nOn **Codex, Pi, and MCP installs there is no bundled binary**: those install skills only, and no install path puts Axiom's tools on `PATH`. Check with `command -v xcui` before using any `xcui` command in this file.\n\nIf it is absent, you have two working options and should say which you took:\n- Put it on PATH — clone the repo and symlink `.claude-plugin/plugins/axiom/bin/xcui` (see the Pi install guide). This is the only way to get `resize sweep`, `wait`, `assert`, `a11y`, `dialog`, and `voiceover`, which have no AXe equivalent.\n- For **input only**, call AXe directly: `axe tap`/`type`/`swipe` take the same flags the `xcui` passthrough forwards — add `--tap-style physical` to every `axe tap` (see Tap styles) — and AXe installs with `brew install cameroncooke/axe/axe`. You then own the `DEVELOPER_DIR` handling yourself — rare, since AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided.\n\n## Prerequisite: run `xcui doctor`\n\n`xcui doctor` verifies AXe (the input/tree engine), Homebrew, Xcode, and a booted sim. If AXe is missing and brew is present, `xcui doctor --install` runs `brew install cameroncooke/axe/axe` (explicit/consented — never silent). Exit 0 = ready to drive; exit 2 = something would stop a device command — AXe missing, no booted sim, several booted with no `--udid`, or an AXe too old for the tap style xcui sends (AXe 1.7.0 added `--tap-style`). Read `problems`/`next_steps` in the JSON; `booted` lists every booted device, so a `--udid` can be picked from it without parsing prose.\n\n**More than one booted sim → pass `--udid` on every command.** Without it, every device verb refuses with exit 2 and lists each booted sim's UDID, name, and runtime, so the retry is one step. It refuses rather than guessing because a guess drives the wrong device while every tap still prints ✓. With one booted sim, `--udid` is optional. `doctor` doesn't refuse — it reports the devices in `booted` and fails the gate, leaving `booted_udid` empty because nothing may be targeted in that state.\n\n## Subcommands\n\n- `xcui wait --for-element | --gone | --idle [--timeout 10s] [--poll 250ms]` — poll the a11y tree until a condition holds. Replaces sleep/re-screenshot guesswork (CLI `waitForExistence`).\n- `xcui assert --id [--label ] [--value ] [--trait ] [--single]` — assert on an element. `--single` checks the id resolves to exactly one element (e.g. \"hero announces as one element\").\n- `xcui a11y set --toggle --value [--app ]` — set an accessibility setting. Supported toggles (all verified against the simulator):\n - `dynamic-type` — native `simctl ui content_size`; `--value` is a size (`large`, `accessibility-extra-large`, … up to `accessibility-extra-extra-extra-large`). Applies live; no relaunch.\n - `increase-contrast` — native `simctl ui increase_contrast`; `--value` is `on`/`off`. Applies live; no relaunch.\n - `reduce-motion` — `defaults write com.apple.Accessibility ReduceMotionEnabled`; needs relaunch, so pass `--app ` to have xcui terminate + relaunch the app.\n - `reduce-transparency` — `defaults write com.apple.Accessibility ReduceTransparencyEnabled`; needs relaunch (pass `--app`).\n - `voiceover` — `devicectl device settings voiceover --enable|--disable`; `--value` is `on`/`off`. Applies live; no relaunch. The only toggle that leaves the simctl/defaults world — simctl has no VoiceOver setter. Read the state back with `xcrun devicectl device info voiceover -d `.\n- `xcui a11y reset` — clear xcui-set overrides (delete the defaults keys, content_size → large, increase_contrast → disabled).\n- `xcui dialog accept | dismiss [--udid ]` — find the frontmost system alert and tap the right button with a physical touch: `accept` prefers the most-permissive standard grant (`Allow While Using App` › `Allow Once` › `Allow` › `OK` › `Open`), `dismiss` prefers the decline (`Don't Allow` › `Cancel` › `Not Now`). A one-button alert is tapped for either intent. Matching is case- and apostrophe-insensitive (curly `’` = straight `'`). The tap delegates to `axe tap` (by id when present, else by label). Exit `0` handled, `1` no actionable alert.\n- `xcui dialog pregrant … [--udid ]` — grant permissions ahead of time via `simctl privacy … grant`, so the dialog never appears. Services are `simctl privacy` names (`camera`, `photos`, `location`, `microphone`, `contacts`, …). Prefer this over `accept` when you control the test setup — no alert means nothing to race.\n- `xcui voiceover traverse [--udid ]` — emit the **computed** VoiceOver announcement sequence: walk the a11y tree in focus order (top-to-bottom, leading-to-trailing) and render each focusable element as `label, value, trait` (plus `dimmed` when disabled). Output is a `sequence` JSON array.\n- `xcui voiceover assert --sequence [--udid ]` — compare the live announcement sequence to an expected one; the file may be a bare JSON string array **or** a saved `traverse` report (it round-trips). Reports every differing index (one entry per mismatched position, plus a length-mismatch note when counts differ); exit `1` on any mismatch.\n\n> **VoiceOver scope (honest framing):** `voiceover` renders the *computed* announcement from the accessibility tree — what VoiceOver would say, derived deterministically. It is **not** captured audio/TTS, which the simulator does not expose to scripting. Use it to catch missing labels, wrong trait phrasing, bad focus order, and unannounced state — not to verify the speech synthesizer itself.\n\n> **Turning VoiceOver ON is not the same as `xcui voiceover`.** Two different things, easy to conflate:\n> - `xcui a11y set --toggle voiceover --value on` — actually starts the screen reader on the device.\n> - `xcui voiceover traverse` / `assert` — **computes** the announcement sequence from the accessibility tree, with VoiceOver *off*. This is the one you want in CI: deterministic, no speech, no focus stealing.\n>\n> You do not need the toggle to run `traverse`. Reach for the toggle when you need the device in a real VoiceOver state — verifying that your app behaves correctly while a screen reader is actually running.\n\n> **Still not supported (a11y toggles):** `differentiate-without-color` and `bold-text` have no confirmable simulator mechanism — no native `simctl ui` setter, candidate `defaults` keys not honored by iOS on the sim, and `devicectl device settings` offers only appearance, audio, biometrics, reset, and voiceover. Omitted rather than shipped unverified.\n\n## Resize sweeps `OS27`\n\n`xcui resize sweep --sizes 400x900,900x600,1100x500 [--assert-id ] [--corner-radius R] [--settle 3s]` drives a resizable-app session across breakpoints and asserts at each one, in a single JSON envelope. It wraps the three tedious parts of `devicectl device appResize`:\n\n- **Session lifecycle.** `appResize start` holds the session only while it runs, so it must be backgrounded and killed. xcui owns that; interrupting a sweep does not strand the app on the Resizable display.\n- **Actual vs requested size.** xcui reports both and flags `honored: false` when they differ. Some devices clamp — an iPhone 17 turned a requested `1100x500` into `1100x550`. A sweep that trusted the request would report a clean pass for a width it never exercised.\n- **Post-resize flakiness.** The automation session can time out once while the display transitions, so each assertion retries before failing.\n\n`--screenshot-dir ` writes `x.png` per breakpoint, captured from the Resizable display. **Reach for this before `--assert-id`**: an id-presence check catches \"the element vanished\", but resizing breaks layouts by overlap, truncation, and clipping — which only a picture shows.\n\n`--strict` fails any step the device clamped. Without it a clamped sweep still exits `0` when the assertions pass, so an exit-code-only consumer reads \"all breakpoints validated\" for breakpoints that were never produced; the `clamped` count and a `note` say so either way.\n\nExit `0` all sizes passed · `1` an assertion, screenshot, or (under `--strict`) a clamp failed · `2` environment error.\n\nThree `devicectl` failures are separated, because they need different fixes:\n\n| CoreDeviceError | Means | Fix |\n|---|---|---|\n| 1001 | device has no Resizable App Management | use an OS 27 simulator or device |\n| 24001 | nothing in the foreground to move | launch the app first |\n| 24004 | no session active | not an error during startup — xcui polls through it |\n\n## Device state — biometrics, orientation, location: use `devicectl`\n\n`xcui` drives the **in-app UI + accessibility tree** (`tap`/`assert`, VoiceOver order) and toggles **accessibility settings** (`a11y set`: Dynamic Type, contrast, motion). It does **not** drive hardware/device state. For **biometrics** (Face ID / Touch ID — neither `xcui` nor `simctl` can do this), orientation, location, status bar, and memory-pressure, use `devicectl`: it works on simulators in Xcode 26.6+, takes one `-d ` selector across sim + device, and emits a stable `--json-output`. The two compose — `devicectl` sets the state, `xcui` asserts the resulting UI. Full verified catalog: `skills/device-control-ref.md`.\n\n## Input — `xcui tap`, not `axe tap`\n\nInput verbs forward to AXe: the flags you pass go through untouched, with the same output and exit code. Use them instead of calling `axe` directly and the SimulatorKit/`DEVELOPER_DIR` handling and the physical tap style come along automatically, so guidance can't drift out of them.\n\nForwarded: `tap`, `slider`, `type`, `swipe`, `drag`, `touch`, `gesture`, `button`, `key`, `key-sequence`, `key-combo`, `screenshot`. `--udid` is injected when omitted and exactly one sim is booted. `xcui tap --help` shows AXe's own flags.\n\n```bash\nxcui tap --id loginButton --udid # physical touch down/up\nxcui type \"user@example.com\" --udid \nxcui touch -x 200 -y 400 --down --up --delay 1.2 --udid # long press — tap has no hold option\naxe describe-ui --udid # raw a11y tree (xcui assert/wait parse this)\n```\n\n### Tap styles\n\nAXe's `tap` takes `--tap-style automatic|simulator|physical`. AXe's default, `automatic`, sends a physical touch only to switches and toggles and FBSimulator `tapAt` to everything else — and `tapAt` activated none of the targets below:\n\n| SwiftUI target | `automatic` | `simulator` | `physical` |\n|---|---|---|---|\n| `Toggle` / switch | fires (the one target `automatic` sends a physical touch to) | no effect | fires |\n| `Button` | no effect | no effect | fires |\n| `List` row (`NavigationLink`) | no effect | no effect | pushes |\n| `Button` inside a `List` | no effect | no effect | fires |\n| `Menu` | no effect | no effect | opens |\n| Row of an open `Menu` | no effect | no effect | fires |\n| Tab of a `TabView` (iPhone) | no effect | no effect | selects |\n\nThe switch row is AXe's documented behavior for `automatic` (\"physical touch for switches/toggles and simulator tap for other targets\"); the rest was measured 2026-09-19 with Xcode 27.1 and AXe 1.8.0 on iPhone 17 (iOS 27.0) and iPhone Duo (iOS 27.1) simulators, where AXe printed `✓ … completed successfully` for every no-effect cell. So `xcui tap` adds `--tap-style physical` when you pass no style (pass one to override), and `xcui dialog` taps physically. Calling `axe tap` yourself? Add `--tap-style physical`.\n\n**Verify the effect, never the ✓.** A tap that changes nothing prints the same success line as one that works. After any tap that matters, confirm the result with `xcui wait --for-element`, `xcui assert`, or a screenshot.\n\n**Selectors tap the accessibility frame, not the pixels.** `--id` and `--label` resolve to the element's accessibility activation point. When a view's accessibility frame is skewed — a label hidden with `.fixedSize()` plus `.frame(width: 0).clipped()` is one measured case, where the tap landed about 27 pt from the visible control — the tap misses and still prints ✓. Screenshot, measure the visible center, and tap `-x/-y` in points, or fix the view so its frame matches what it draws.\n\n> **Still calling `axe` directly?** `describe-ui`, `stream-video`, and `record-video` stay bare — the first is what xcui itself parses, and the two streaming verbs outlive any request timeout. Those are the only calls that need a `DEVELOPER_DIR=` prefix, and only when `xcui doctor` reports an `axe_developer_dir` (rare — AXe 1.8.0 loads SimulatorKit under Xcode 27 unaided).\n\n## Output & exit codes\n\nJSON by default (`tool`/`version` envelope); `--human` for prose. Exit: `0` pass · `1` assertion-fail/wait-timeout · `2` environment error · `8` output-write error.\n\n> **CLI gotcha:** Go's flag parser stops at the first positional, so always put flags after the subcommand and use the all-flag forms shown above (`assert --id …`, not `assert …`).\n\n## Resources\n\n**Tools**: `axe` (AXe — `brew install cameroncooke/axe/axe`), `xcrun simctl`\n\n**Skills**: axiom-accessibility, axiom-testing\n\n**Agents**: simulator-tester (drives xcui live), accessibility-auditor (static a11y scan)\n", "skillType": "reference", "source": "axiom", "tags": [ @@ -41588,62 +41588,62 @@ "level": 1, "startLine": 0, "endLine": 3, - "charCount": 456 + "charCount": 496 }, { "heading": "Invocation", "level": 2, "startLine": 4, "endLine": 13, - "charCount": 957 + "charCount": 1023 }, { "heading": "Prerequisite: run `xcui doctor`", "level": 2, "startLine": 14, - "endLine": 17, - "charCount": 568 + "endLine": 19, + "charCount": 1112 }, { "heading": "Subcommands", "level": 2, - "startLine": 18, - "endLine": 43, - "charCount": 4505 + "startLine": 20, + "endLine": 45, + "charCount": 4527 }, { "heading": "Resize sweeps `OS27`", "level": 2, - "startLine": 44, - "endLine": 65, + "startLine": 46, + "endLine": 67, "charCount": 1966 }, { "heading": "Device state — biometrics, orientation, location: use `devicectl`", "level": 2, - "startLine": 66, - "endLine": 69, + "startLine": 68, + "endLine": 71, "charCount": 704 }, { "heading": "Input — `xcui tap`, not `axe tap`", "level": 2, - "startLine": 70, - "endLine": 83, - "charCount": 1079 + "startLine": 72, + "endLine": 106, + "charCount": 3261 }, { "heading": "Output & exit codes", "level": 2, - "startLine": 84, - "endLine": 89, + "startLine": 107, + "endLine": 112, "charCount": 376 }, { "heading": "Resources", "level": 2, - "startLine": 90, - "endLine": 97, + "startLine": 113, + "endLine": 120, "charCount": 230 } ], @@ -43007,7 +43007,7 @@ "axiom-uikit--uikit-modernization": { "name": "axiom-uikit--uikit-modernization", "description": "The 27 cycle makes the **scene-based life cycle mandatory** and assumes every app is resizable. This is the highest-impact UIKit change in years: it is a launch-time breaking change, not an opt-in.", - "content": "# UIKit App Modernization — Scene Lifecycle & Resizability\n\nThe 27 cycle makes the **scene-based life cycle mandatory** and assumes every app is resizable. This is the highest-impact UIKit change in years: it is a launch-time breaking change, not an opt-in.\n\n## The breaking change — UIScene is required at 27\n\nWhen you build against the 27 SDKs, **an app with only a `UIApplicationDelegate` (no `UISceneDelegate`) will no longer launch.** You must adopt the scene-based life cycle.\n\n- Migration path: WWDC 2025 \"Make your UIKit app more flexible\" + Apple's doc \"Transitioning to the UIKit scene-based life cycle.\"\n- The SDK reflects this: `UIApplicationDelegate.application(_:supportedInterfaceOrientationsForWindow:)` and `UIApplication.supportedInterfaceOrientationsForWindow(_:)` are **deprecated at iOS 27** in favor of `UIWindowSceneDelegate.supportedInterfaceOrientations(for:)`.\n\nThis is a behavior/requirement change, so it carries no additive `OS27` marker — but it gates launch. Treat it as a must-fix before building against the 27 SDK.\n\n#### The Info.plist side of the migration\n\n- **Scene manifest** — `UIApplicationSceneManifest` declares the scenes. `UIApplicationSupportsMultipleScenes` (Bool) opts into simultaneous scenes (iPad multi-window, macOS windows); `UISceneConfigurations` holds the default configuration the system uses to create new scenes — under the `UIWindowSceneSessionRoleApplication` role, each entry carries `UISceneConfigurationName` and, conventionally, `UISceneDelegateClassName` (`UISceneStoryboardFile` for storyboard apps):\n ```xml\n UIApplicationSceneManifest\n \n UIApplicationSupportsMultipleScenes\n UISceneConfigurations\n \n UIWindowSceneSessionRoleApplication\n \n \n UISceneConfigurationNameDefault\n UISceneDelegateClassName$(PRODUCT_MODULE_NAME).SceneDelegate\n \n \n \n \n ```\n- **Launch screen is validated at upload** — apps built with the iOS 27 SDK or later must declare one of `UILaunchScreen`, `UILaunchStoryboardName`, `UILaunchScreens`, `UILaunchStoryboards` or the build fails validation at App Store Connect upload (TN3208; iPhone and iPad, App Store and alternative marketplaces). The storyboard-free path is a `UILaunchScreen` dict with `UIColorName`/`UIImageName`. Submission checklist: axiom-shipping (skills/app-store-submission.md).\n- **Destinations, not device checks** — supported destinations/device families are target settings (`TARGETED_DEVICE_FAMILY`); runtime code adapts via size classes and scene geometry, never by re-deriving \"what device am I on\". Mac availability for iOS apps is an App Store Connect setting — see axiom-macos (skills/ios-apps-on-mac.md).\n\n## Scene lifecycle edges\n\nThe connect/foreground/background arc is the easy part; these are the edges apps get wrong:\n\n- **Teardown** — `sceneDidDisconnect(_:)` fires when the user closes the window in the app switcher **or when the system reclaims memory** — it is not app termination. Release scene-scoped resources and save user data here; don't treat it as \"the app is quitting\".\n- **Opening scenes from UIKit** — the peer of SwiftUI's `openWindow` is an activation request (`requestSceneSessionActivation` is slated for deprecation):\n\n ```swift\n var request = UISceneSessionActivationRequest()\n request.userActivity = NSUserActivity(activityType: \"com.example.detail\")\n UIApplication.shared.activateSceneSession(for: request) { error in\n // request failed — log it\n }\n ```\n\n `UISceneSessionActivationRequest(role:)` and `(session:)` target a specific role or an existing session; attaching a `userActivity` is how content reaches the new scene. A scene's `activationConditions` tune which existing scene the system picks for a given target.\n- **Drag creates windows** — with `UIApplicationSupportsMultipleScenes`, a drag item carrying an `NSUserActivity` lets the user open that content as a new window by dropping it at the iPad screen edge (UIKit ships the item-provider conformance — `NSUserActivity+NSItemProvider.h`):\n\n ```swift\n let activity = NSUserActivity(activityType: \"com.example.detail\")\n activity.userInfo = [\"id\": document.id]\n let item = UIDragItem(itemProvider: NSItemProvider(object: activity))\n ```\n\n The `activityType` must also be listed in Info.plist under `NSUserActivityTypes` — without it the drag silently creates no window. Route the activity in `scene(_:willConnectTo:options:)` exactly as for an activation request. SwiftUI's `.onDrag` can vend the same `NSItemProvider`. Pair the activity with your data representations on one item so a drop into another app still transfers content.\n- **Per-window undo** — `UIResponder.undoManager` resolves up the responder chain, and SwiftUI's `@Environment(\\.undoManager)` is per-window. The multi-window bug is holding one `UndoManager` in a shared model singleton: ⌘Z in one window then undoes edits made in another. Keep the manager scene-scoped — use the environment/responder-chain instance, or own one per scene root, and register undo actions against the manager of the window where the edit happened.\n- **Per-scene restoration** — return the window's state from `stateRestorationActivity(for:)`; at the next connect, read it back from `session.stateRestorationActivity` inside `scene(_:willConnectTo:options:)`. This is the UIKit peer of SwiftUI's `@SceneStorage` — per window, not per app.\n\n ```swift\n func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {\n let activity = NSUserActivity(activityType: \"com.example.browse\")\n activity.userInfo = [\"path\": currentPath]\n return activity\n }\n ```\n- **External displays** — a non-interactive external screen is its own scene session under `UIWindowSceneSessionRoleExternalDisplayNonInteractive` (iOS 16; replaces the deprecated unqualified `UIWindowSceneSessionRoleExternalDisplay`). Declare a configuration for that role and the system connects a scene when a screen appears — no `UIScreen` notifications.\n\n#### Scene accessories — declare the content, let the system present it `OS27`\n\nA **scene accessory** declares supplementary content up front; the system presents it on the app's behalf when the associated functionality becomes available (at 27, an external display connecting). `iOS27` only — unavailable on Mac Catalyst, tvOS, visionOS and watchOS.\n\n```swift\n@MainActor\nfinal class PresenterViewController: UIViewController {\n private var accessory: UISceneAccessoryRegistration?\n\n func present(deckID: String) {\n let configuration = UISceneConfiguration(name: \"Presentation\")\n configuration.delegateClass = PresentationSceneDelegate.self\n accessory = registerSceneAccessory(\n .externalNonInteractive(sceneConfiguration: configuration, userInfo: deckID))\n }\n\n override func updateProperties() {\n super.updateProperties()\n accessory?.isEnabled = accessory?.isAvailable == true\n }\n}\n```\n\n| Member | Role |\n|---|---|\n| `UISceneAccessory.externalNonInteractive(sceneConfiguration:)` / `(sceneConfiguration:userInfo:)` | the only accessory kind at 27 — non-interactive content on an external display |\n| `UIViewController.registerSceneAccessory(_:)` | registers it; returns a `UISceneAccessoryRegistration` handle |\n| `UISceneAccessoryRegistration.isAvailable` | read-only, the system's call — observable during the `updateProperties` and `layoutSubviews` lifecycle events |\n| `UISceneAccessoryRegistration.isEnabled` | read-write, your opt-in; it does **not** reflect availability |\n| `UIScene.ConnectionOptions.sceneAccessoryUserInfo` | reads the `userInfo` back in `scene(_:willConnectTo:options:)` |\n| `UIViewController.unregisterSceneAccessory(_:)` | unregisters, dismissing the content if it is being presented |\n\n- **The app must remain fully functional without the accessory** — it enhances the experience, it never gates it. Availability is the system's decision; enabling an unavailable accessory does not present it.\n- `UISceneConfiguration(name:)` (`iOS27`/`macCatalyst27`/`tvOS27`/`visionOS27`, not watchOS) is a new role-free initializer — sessions created from it have their role set automatically by the system, so an accessory's configuration doesn't name a session role at all.\n\n#### Adaptation traits beyond size classes\n\n`UITraitCollection` carries more adaptation inputs than the size classes: `displayGamut` (iOS 10 — P3 vs sRGB asset decisions), `legibilityWeight` (iOS 13 — Bold Text accessibility setting), and `activeAppearance` (iOS 14 — whether the UI should draw its active or inactive appearance; varies with window foreground state on macOS and in iPad Stage Manager/windowed modes). Observe any of them with `registerForTraitChanges` (see `skills/adaptive-layout.md`). There is **no public pointer-capability trait** — pointer presence is discovered through the interaction APIs themselves (see Desktop-class input below).\n\n## Every app is now resizable\n\niPhone apps resize freely (iPhone Mirroring on Mac; an iPhone-only app on iPad). Your UI must adapt to **any** scene size at runtime. iPhone Duo — two displays, a fold, bars on the side — builds on this model; see axiom-swiftui (skills/iphone-duo.md).\n\n#### Stop reading the screen and the idiom\n\n| Don't (wrong in resizable / external-display contexts) | Do |\n|---|---|\n| `UIScreen.main` | `window.windowScene?.screen` |\n| `screen.scale` | `traitCollection.displayScale` |\n| `screen.bounds` | the view's own `bounds`, or `windowScene.effectiveGeometry.coordinateSpace.bounds` |\n| `UIDevice.userInterfaceIdiom` for layout | **size classes** (`traitCollection.horizontalSizeClass`) |\n| `supportedInterfaceOrientations` for layout | size classes — orientation is only a *preference* at 27 and is ignored in resizable environments |\n\n`effectiveGeometry` (iOS 16) and the `windowScene(_:didUpdateEffectiveGeometry:)` delegate (iOS 26) are the adaptive-geometry APIs to adopt — they predate 27, but 27 is where ignoring them breaks. `UIRequiresFullScreen` is now honored on iPhone but only enables *discrete* resizing that snaps to orientation-honoring configurations (for games); it no longer fully opts out of resizing.\n\n```swift\noverride func layoutSubviews() {\n super.layoutSubviews()\n let displayScale = traitCollection.displayScale // not UIScreen.main.scale\n // size from self.bounds, not the screen\n}\n\nfunc windowScene(_ windowScene: UIWindowScene,\n didUpdateEffectiveGeometry previous: UIWindowScene.Geometry) {\n let bounds = windowScene.effectiveGeometry.coordinateSpace.bounds\n}\n```\n\n#### Express preferences, not a fixed canvas\n\nYou no longer own a fixed canvas — you express preferences the user and system honor.\n\n- **Minimum size** — the documented replacement for the old `UIRequiresFullScreen` opt-out (TN3192). Set it on the scene's `UISceneSizeRestrictions` so users can't shrink the window below a usable size:\n ```swift\n windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 600)\n ```\n- **Orientation lock** — a *preference*, not a guarantee, in resizable environments. Override `UIViewController.prefersInterfaceOrientationLocked` (returns `Bool`) and call `setNeedsUpdateOfPrefersInterfaceOrientationLocked()` when it changes; read the resolved state from `windowScene.effectiveGeometry.isInterfaceOrientationLocked` (iOS 26).\n- **Interactive vs settled resize** — `UIWindowSceneGeometry.isInteractivelyResizing` (iOS 26) is `true` while the user drags; throttle expensive work during the drag and settle when it clears. SwiftUI's equivalent is `.onInteractiveResizeChange(_:)` (see axiom-swiftui (skills/layout-ref.md)).\n\n## iPhone Mirroring compatibility\n\nUnder iPhone Mirroring the app keeps running on the iPhone; the Mac supplies indirect mouse/trackpad/keyboard input and, from the 27 cycle, a freely resizable window. Standard UIKit and SwiftUI controls receive translated input correctly — the compatibility work is in custom gestures, biometric auth, and orientation assumptions.\n\n#### The orientation trap\n\nA mirrored app **always reports portrait interface orientation, regardless of the window's aspect ratio**. A landscape-shaped mirrored window is still \"portrait\". Never derive layout from `interfaceOrientation` — use size classes and the scene's `effectiveGeometry` (see the table above).\n\n#### Indirect input reaches custom gestures differently\n\nWith `UIApplicationSupportsIndirectInputEvents` in effect (Info.plist key, iOS 13.4; treated as enabled when built against iOS 17+ SDKs):\n\n- Pointer clicks arrive as `UITouch` of type `.indirectPointer`, not `.direct`.\n- Trackpad pinch and rotate arrive as `UIEvent.EventType.transform` events that drive **only** `UIPinchGestureRecognizer` and `UIRotationGestureRecognizer`. For these events `numberOfTouches` is `0` and `location(ofTouch:in:)` raises — a custom recognizer that reads individual touches must detect non-touch events first (e.g. in `shouldReceive(_:)`).\n- Scroll input is not touch: `allowedScrollTypesMask` (iOS 13.4) controls which scroll types a custom pan recognizer receives. `.discrete` is wheel-mouse scrolling, `.continuous` is trackpad — Mirroring users bring either device, so set `.all` explicitly:\n ```swift\n panRecognizer.allowedScrollTypesMask = .all // wheel mice AND trackpads\n ```\n `UIScrollView` handles both automatically; this is only for custom pan handling.\n\n#### Biometric auth needs a companion path\n\nBiometric requests fail by default under Mirroring — the iPhone's Face ID/Touch ID sensors are not accessible from the Mac (TN3210). Use the companion-capable Local Authentication policy (iOS 18) so people can approve on the Mac (or a paired Apple Watch) instead:\n\n```swift\nlet context = LAContext()\ntry await context.evaluatePolicy(\n .deviceOwnerAuthenticationWithBiometricsOrCompanion,\n localizedReason: \"Unlock your vault\")\n```\n\nWith no companion nearby it behaves exactly like `.deviceOwnerAuthenticationWithBiometrics`, so it is safe as the default policy. On iOS the companion types are the Mac (iOS 18) and Vision Pro (iOS 26) — `LACompanionType` has no Watch case on iOS. A companion-only request with no companion available throws `LAError.companionNotAvailable`. For keychain items gated by `SecAccessControl`, add the `.companion` flag (iOS 18) alongside biometry — see axiom-security (skills/keychain.md).\n\n#### Drag and drop crosses devices\n\nStandard drag interactions (`UIDragInteraction`/`UIDropInteraction`, SwiftUI `Transferable`) participate in iPhone↔Mac drag and drop automatically (iOS 18.1/macOS 15.1). No Mirroring-specific API exists — apps that already implement standard drag and drop get the cross-device behavior for free. See axiom-swift (skills/transferable-ref.md).\n\n#### Test it for real\n\nIterate with `devicectl device appResize` — scriptable, so breakpoints can be swept and asserted automatically (see axiom-tools (skills/device-control-ref.md)) — or by hand in Device Hub's resizable simulator. Then validate in **actual iPhone Mirroring on macOS 27**; Apple's guidance is resizable simulator first, real devices and Mirroring to confirm. TN3210's checklist includes verifying that pinch, rotate, and scroll gestures work with a trackpad or mouse.\n\n## Desktop-class input — pointer, hardware keyboard, Pencil\n\nThe same environments that resize your windows also bring pointers, hardware keyboards, and (on iPad) the Pencil. UIKit's opt-in surface:\n\n- **Pointer effects** — `UIPointerInteraction` (iOS 13.4) gives views the system hover treatment; return a `UIPointerStyle` from the delegate to shape the region. `UIHoverGestureRecognizer` (iOS 13) tracks pointer movement over a view (plus Pencil hover `zOffset` from iOS 16.1).\n- **Key commands** — register `UIKeyCommand`s on responders (or via `UIMenuBuilder`, which doubles as the iPad menu bar). The iPad hold-⌘ shortcut HUD shows each command's `discoverabilityTitle`, falling back to its `title`; a command with neither is invisible to discovery.\n- **Scribble** — Pencil handwriting into text fields is automatic for anything conforming to `UITextInput` (iOS 14, iPad): `UITextField`, `UITextView`, SwiftUI `TextField` all work with zero code. The work is only at the edges: `UIScribbleInteraction` (delegate) disables or tunes writing per view — e.g. suppress it on a field with a custom input view; `UIIndirectScribbleInteraction` makes UI that doesn't *look* like a text field writable (a tap-to-edit title, a canvas that should accept handwritten labels) by vending virtual writable \"elements\". A custom text editor must conform to `UITextInput` to participate at all.\n- SwiftUI equivalents (`onHover`, `hoverEffect`, `keyboardShortcut`) live in axiom-swiftui (skills/gestures.md Pattern 8).\n\n```swift\nbutton.addInteraction(UIPointerInteraction(delegate: self))\n\noverride var keyCommands: [UIKeyCommand]? {\n let find = UIKeyCommand(input: \"f\", modifierFlags: .command,\n action: #selector(focusSearch))\n find.discoverabilityTitle = \"Find\"\n return [find]\n}\n```\n\n## Navigation bar titles and subtitles (iOS 26)\n\n`UINavigationItem` gained a subtitle, attributed titles, and separate content under the large title. Apple's Mail example puts the unread count in `subtitle` and, while filtering, a filter button in `largeSubtitleView`:\n\n```swift\nnavigationItem.title = \"Inbox\"\nnavigationItem.subtitle = \"49 Unread\"\nnavigationItem.largeSubtitleView = filterButton\n```\n\n| Slot | String | Attributed (beats string) | Custom view (beats both) | When all are nil |\n|---|---|---|---|---|\n| Inline title | `title` | `attributedTitle` | `titleView` | no title |\n| Inline subtitle | `subtitle` | `attributedSubtitle` | `subtitleView` | no subtitle |\n| Large title | `largeTitle` | — | — | uses `title` |\n| Large subtitle | `largeSubtitle` | `largeAttributedSubtitle` | `largeSubtitleView` | falls back to the `subtitle` string |\n\nIn Swift the attributed properties are `AttributedString?`. When `titleView` is non-nil, `attributedSubtitle` is ignored too. Everything except `title`/`titleView` is iOS 26 and unavailable on tvOS, watchOS, and visionOS.\n\nVerified on iPhone, iOS 26.5 and 27.0:\n- **`largeSubtitleTextAttributes` is ignored.** A foreground color in it had no effect whether set on the item's appearances, the bar's, or `UINavigationBar.appearance()`; the large subtitle took `subtitleTextAttributes`, even with `largeSubtitle` set. Style it with `largeAttributedSubtitle`, and the inline bar with `attributedSubtitle`; both render their attributes. Prefer attributed strings to appearance objects anyway, since the new design asks apps to drop bar background customization.\n- **`largeSubtitleView` disappears when the bar collapses**, and the inline bar shows `subtitle`. Set `subtitle` too when the status must survive scrolling.\n- **A `UIButton` in `largeSubtitleView` renders centered.** Set `contentHorizontalAlignment = .leading` and zero `contentInsets` in its configuration to align it under the large title.\n- **Custom views win their slot even when empty.** A leftover `UIView()` in `subtitleView` or `largeSubtitleView` silently hides that slot's string. To remove a subtitle, nil every variant.\n- **Subtitles need the new design.** Nothing renders in the compatibility mode `UIDesignRequiresCompatibility` requests, which applies on iOS 26.x and, for apps still built with the 26 SDK, on iOS 27 (axiom-design (skills/liquid-glass.md)). Before 26 or in compatibility mode, compose a two-line `titleView`, and set it only on that path, because a non-nil `titleView` replaces the native title. There is no runtime check for compatibility mode, so decide by build: a 27-SDK app that keeps the key sets `subtitle` only under `#available(iOS 27, *)` and the `titleView` on 26.x; a 26-SDK app with the key uses the `titleView` on every OS.\n\nLarge titles now sit at the top of the scroll view and scroll with the content. Extend the scroll view fully under the navigation bar so the large title stays visible.\n\nSwiftUI peers (`.navigationSubtitle` plus the `.title`, `.subtitle`, `.largeTitle`, and `.largeSubtitle` toolbar placements) are in axiom-swiftui (skills/toolbars.md) Pattern 14.\n\n## New 27 additive APIs\n\n| API | Scope | Use |\n|-----|-------|-----|\n| `UITabBarController.prominentTabIdentifier` | `iOS27`/`visionOS27` | mark one tab always-visible/prominent |\n| `UITabBarControllerSidebar.preferredPlacement` (`.sidebar`) + `Placement` | `iOS27`/`visionOS27` | iPhone can now opt a tab bar into a sidebar (the `sidebar` object itself is iOS 18) |\n| `UINavigationItem.navigationBarMinimization` (`UIBarMinimization`: `minimizationBehavior`/`safeAreaAdjustment`/`restorationBehavior`) | `iOS27` — types also tvOS/visionOS, but see below | control how the nav bar minimizes on scroll; SwiftUI peers are the `toolbarMinimization*` modifiers — see axiom-swiftui (skills/toolbars.md) Pattern 12 |\n| `UIMenuElement.preferredImageVisibility` | `iOS27` | Liquid Glass may hide menu images by default; opt an item back in |\n| `CMMotionManager.deviceMotionBody` | `iOS27`/`watchOS27`/`visionOS27` | assign a `UIView` as the motion reference frame (Body protocols) |\n| `CLLocationManager.headingBody` | `iOS27`/`macOS27`/`watchOS27` | replaces the deprecated `headingOrientation` |\n| `UITraitCollection.systemPrefersReducedResourceUsage` (+ `UITraitSystemPrefersReducedResourceUsage`, `.systemPrefersReducedResourceUsageDidChange`) | `iOS27`/`tvOS27`/`visionOS27` | system asks the app to cut discretionary work under resource pressure — react via `registerForTraitChanges`; see `axiom-performance (energy.md)` for the response playbook |\n\n`UIView` conforms to the CoreMotion/CoreLocation Body protocols, so you set `motionManager.deviceMotionBody = view` / `locationManager.headingBody = view` directly.\n\n#### Nav bar minimization in depth `OS27`\n\nIn Swift `UIBarMinimization` is a **struct** (the ObjC class is refined for Swift), read and written through `UINavigationItem.navigationBarMinimization`:\n\n```swift\nitem.navigationBarMinimization.minimizationBehavior = .onScrollDown\nitem.navigationBarMinimization.restorationBehavior = .atScrollEdge\nitem.navigationBarMinimization.safeAreaAdjustment = .enabled\n```\n\n| Property | Values | Effect |\n|---|---|---|\n| `minimizationBehavior` | `.automatic`, `.never`, `.onScrollDown`, `.onScrollUp` | whether, and in which scroll direction, the bar minimizes |\n| `safeAreaAdjustment` | `.automatic`, `.enabled`, `.disabled` | `.enabled` lets content reflow into the freed space; `.disabled` keeps the safe area fixed |\n| `restorationBehavior` | `.automatic`, `.atScrollEdge` | `.automatic` restores when the user reverses scroll direction; `.atScrollEdge` only when content reaches the scroll edge |\n\n- **`.atScrollEdge` is *currently* honored only alongside `.onScrollDown`.** Apple's wording is \"Currently this is only honored alongside…\", so treat it as present behavior rather than a guarantee. With any other minimization behavior the system silently falls back to `.automatic` — pairing it with `.onScrollUp` is a no-op, not an error.\n- The system already selects `.atScrollEdge` on its own for navigation items whose `preferredSearchBarPlacement` is `.integratedCentered` (iOS 26).\n- Minimizing the navigation bar also minimizes an **integrated top tab bar**. Only the navigation bar supports customizing `safeAreaAdjustment`.\n- **The three types are iOS/tvOS/visionOS 27, but every case except `.automatic` is iOS-only** — explicitly unavailable on tvOS, visionOS and watchOS. A shared helper that assigns `.onScrollDown` fails to compile for tvOS (`'onScrollDown' is unavailable in tvOS`), so gate the assignment itself, not just the property access.\n\n## Apple Intelligence touchpoints\n\nMenus gain an automatic \"Ask Siri\" affordance, and UIKit adds a View Annotations API to annotate views with `AppEntity`s for Siri context (see WWDC 2026-278). If you support drag and drop, Siri may load resources via your drag handlers — avoid animations/modal UI in `sessionWillBegin` (a drag can start without a gesture); put stateful drag UI in `sessionDidMove`.\n\n## Let Xcode do the mechanical migration\n\nXcode 27 ships an app-modernization agent skill that rewrites `UIScreen.main` calls → `traitCollection`/scene bounds, orientation checks → size classes, and can migrate to the scene life cycle. Export the skill for other tools with `xcrun agent skills export`. See `axiom-xcode-mcp` for the agentic-Xcode workflow. Xcode 27.1 extends this skill to SwiftUI and iPhone Duo under a new name — see axiom-swiftui (skills/iphone-duo.md, Tooling and Testing).\n\n## Resources\n\n**WWDC**: 2025-243, 2025-284, 2026-278\n\n**Docs**: /uikit/app-and-environment, /uikit/uiscenedelegate, /uikit/uiwindowscene, /uikit/uiscenesizerestrictions, /uikit/transitioning-to-the-uikit-scene-based-life-cycle, /uikit/uitabbarcontroller, /uikit/uitabbarcontrollersidebar, /uikit/uimenuelement, /technotes/tn3192-migrating-your-app-from-the-deprecated-uirequiresfullscreen-key, /technotes/tn3210-optimizing-your-app-for-iphone-mirroring, /technotes/tn3208-preparing-your-apps-launch-screen-to-meet-app-store-requirements, /bundleresources/information-property-list/uiapplicationscenemanifest, /bundleresources/information-property-list/uilaunchscreen, /bundleresources/information-property-list/uiapplicationsupportsindirectinputevents, /uikit/uipangesturerecognizer/allowedscrolltypesmask, /localauthentication/lapolicy, /uikit/drag-and-drop, /uikit/uiscribbleinteraction, /uikit/uiindirectscribbleinteraction, /uikit/uiresponder/undomanager, /uikit/uibarminimization, /uikit/uinavigationitem/navigationbarminimization, /uikit/uinavigationitem/subtitle, /uikit/uinavigationitem/largesubtitleview, /uikit/customizing-your-app-s-navigation-bar, /uikit/uisceneaccessory, /uikit/uisceneaccessoryregistration\n\n**Skills**: skills/uikit-bridging.md, axiom-xcode-mcp, axiom-swiftui (size-class-driven adaptive layout), axiom-swiftui (skills/toolbars.md), axiom-security (skills/keychain.md), axiom-swift (skills/transferable-ref.md)\n", + "content": "# UIKit App Modernization — Scene Lifecycle & Resizability\n\nThe 27 cycle makes the **scene-based life cycle mandatory** and assumes every app is resizable. This is the highest-impact UIKit change in years: it is a launch-time breaking change, not an opt-in.\n\n## The breaking change — UIScene is required at 27\n\nWhen you build against the 27 SDKs, **an app with only a `UIApplicationDelegate` (no `UISceneDelegate`) will no longer launch.** You must adopt the scene-based life cycle.\n\n- Migration path: WWDC 2025 \"Make your UIKit app more flexible\" + Apple's doc \"Transitioning to the UIKit scene-based life cycle.\"\n- The SDK reflects this: `UIApplicationDelegate.application(_:supportedInterfaceOrientationsForWindow:)` and `UIApplication.supportedInterfaceOrientationsForWindow(_:)` are **deprecated at iOS 27** in favor of `UIWindowSceneDelegate.supportedInterfaceOrientations(for:)`.\n\nThis is a behavior/requirement change, so it carries no additive `OS27` marker — but it gates launch. Treat it as a must-fix before building against the 27 SDK.\n\n#### The Info.plist side of the migration\n\n- **Scene manifest** — `UIApplicationSceneManifest` declares the scenes. `UIApplicationSupportsMultipleScenes` (Bool) opts into simultaneous scenes (iPad multi-window, macOS windows); `UISceneConfigurations` holds the default configuration the system uses to create new scenes — under the `UIWindowSceneSessionRoleApplication` role, each entry carries `UISceneConfigurationName` and, conventionally, `UISceneDelegateClassName` (`UISceneStoryboardFile` for storyboard apps):\n ```xml\n UIApplicationSceneManifest\n \n UIApplicationSupportsMultipleScenes\n UISceneConfigurations\n \n UIWindowSceneSessionRoleApplication\n \n \n UISceneConfigurationNameDefault\n UISceneDelegateClassName$(PRODUCT_MODULE_NAME).SceneDelegate\n \n \n \n \n ```\n- **Launch screen is validated at upload** — apps built with the iOS 27 SDK or later must declare one of `UILaunchScreen`, `UILaunchStoryboardName`, `UILaunchScreens`, `UILaunchStoryboards` or the build fails validation at App Store Connect upload (TN3208; iPhone and iPad, App Store and alternative marketplaces). The storyboard-free path is a `UILaunchScreen` dict with `UIColorName`/`UIImageName`. Submission checklist: axiom-shipping (skills/app-store-submission.md).\n- **Destinations, not device checks** — supported destinations/device families are target settings (`TARGETED_DEVICE_FAMILY`); runtime code adapts via size classes and scene geometry, never by re-deriving \"what device am I on\". Mac availability for iOS apps is an App Store Connect setting — see axiom-macos (skills/ios-apps-on-mac.md).\n\n## Scene lifecycle edges\n\nThe connect/foreground/background arc is the easy part; these are the edges apps get wrong:\n\n- **Teardown** — `sceneDidDisconnect(_:)` fires when the user closes the window in the app switcher **or when the system reclaims memory** — it is not app termination. Release scene-scoped resources and save user data here; don't treat it as \"the app is quitting\".\n- **Opening scenes from UIKit** — the peer of SwiftUI's `openWindow` is an activation request (`requestSceneSessionActivation` is slated for deprecation):\n\n ```swift\n var request = UISceneSessionActivationRequest()\n request.userActivity = NSUserActivity(activityType: \"com.example.detail\")\n UIApplication.shared.activateSceneSession(for: request) { error in\n // request failed — log it\n }\n ```\n\n `UISceneSessionActivationRequest(role:)` and `(session:)` target a specific role or an existing session; attaching a `userActivity` is how content reaches the new scene. A scene's `activationConditions` tune which existing scene the system picks for a given target.\n- **Drag creates windows** — with `UIApplicationSupportsMultipleScenes`, a drag item carrying an `NSUserActivity` lets the user open that content as a new window by dropping it at the iPad screen edge (UIKit ships the item-provider conformance — `NSUserActivity+NSItemProvider.h`):\n\n ```swift\n let activity = NSUserActivity(activityType: \"com.example.detail\")\n activity.userInfo = [\"id\": document.id]\n let item = UIDragItem(itemProvider: NSItemProvider(object: activity))\n ```\n\n The `activityType` must also be listed in Info.plist under `NSUserActivityTypes` — without it the drag silently creates no window. Route the activity in `scene(_:willConnectTo:options:)` exactly as for an activation request. SwiftUI's `.onDrag` can vend the same `NSItemProvider`. Pair the activity with your data representations on one item so a drop into another app still transfers content.\n- **Per-window undo** — `UIResponder.undoManager` resolves up the responder chain, and SwiftUI's `@Environment(\\.undoManager)` is per-window. The multi-window bug is holding one `UndoManager` in a shared model singleton: ⌘Z in one window then undoes edits made in another. Keep the manager scene-scoped — use the environment/responder-chain instance, or own one per scene root, and register undo actions against the manager of the window where the edit happened.\n- **Per-scene restoration** — return the window's state from `stateRestorationActivity(for:)`; at the next connect, read it back from `session.stateRestorationActivity` inside `scene(_:willConnectTo:options:)`. This is the UIKit peer of SwiftUI's `@SceneStorage` — per window, not per app.\n\n ```swift\n func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {\n let activity = NSUserActivity(activityType: \"com.example.browse\")\n activity.userInfo = [\"path\": currentPath]\n return activity\n }\n ```\n- **External displays** — a non-interactive external screen is its own scene session under `UIWindowSceneSessionRoleExternalDisplayNonInteractive` (iOS 16; replaces the deprecated unqualified `UIWindowSceneSessionRoleExternalDisplay`). Declare a configuration for that role and the system connects a scene when a screen appears — no `UIScreen` notifications.\n\n#### Scene accessories — declare the content, let the system present it `OS27`\n\nA **scene accessory** declares supplementary content up front; the system presents it on the app's behalf when the associated functionality becomes available (at 27, an external display connecting). `iOS27` only — unavailable on Mac Catalyst, tvOS, visionOS and watchOS.\n\n```swift\n@MainActor\nfinal class PresenterViewController: UIViewController {\n private var accessory: UISceneAccessoryRegistration?\n\n func present(deckID: String) {\n let configuration = UISceneConfiguration(name: \"Presentation\")\n configuration.delegateClass = PresentationSceneDelegate.self\n accessory = registerSceneAccessory(\n .externalNonInteractive(sceneConfiguration: configuration, userInfo: deckID))\n }\n\n override func updateProperties() {\n super.updateProperties()\n accessory?.isEnabled = accessory?.isAvailable == true\n }\n}\n```\n\n| Member | Role |\n|---|---|\n| `UISceneAccessory.externalNonInteractive(sceneConfiguration:)` / `(sceneConfiguration:userInfo:)` | the only accessory kind at 27 — non-interactive content on an external display |\n| `UIViewController.registerSceneAccessory(_:)` | registers it; returns a `UISceneAccessoryRegistration` handle |\n| `UISceneAccessoryRegistration.isAvailable` | read-only, the system's call — observable during the `updateProperties` and `layoutSubviews` lifecycle events |\n| `UISceneAccessoryRegistration.isEnabled` | read-write, your opt-in; it does **not** reflect availability |\n| `UIScene.ConnectionOptions.sceneAccessoryUserInfo` | reads the `userInfo` back in `scene(_:willConnectTo:options:)` |\n| `UIViewController.unregisterSceneAccessory(_:)` | unregisters, dismissing the content if it is being presented |\n\n- **The app must remain fully functional without the accessory** — it enhances the experience, it never gates it. Availability is the system's decision; enabling an unavailable accessory does not present it.\n- `UISceneConfiguration(name:)` (`iOS27`/`macCatalyst27`/`tvOS27`/`visionOS27`, not watchOS) is a new role-free initializer — sessions created from it have their role set automatically by the system, so an accessory's configuration doesn't name a session role at all.\n\n#### Adaptation traits beyond size classes\n\n`UITraitCollection` carries more adaptation inputs than the size classes: `displayGamut` (iOS 10 — P3 vs sRGB asset decisions), `legibilityWeight` (iOS 13 — Bold Text accessibility setting), and `activeAppearance` (iOS 14 — whether the UI should draw its active or inactive appearance; varies with window foreground state on macOS and in iPad Stage Manager/windowed modes). Observe any of them with `registerForTraitChanges` (see `skills/adaptive-layout.md`). There is **no public pointer-capability trait** — pointer presence is discovered through the interaction APIs themselves (see Desktop-class input below).\n\n## Every app is now resizable\n\niPhone apps resize freely (iPhone Mirroring on Mac; an iPhone-only app on iPad). Your UI must adapt to **any** scene size at runtime. iPhone Duo — two displays, a fold, bars on the side — builds on this model; see axiom-swiftui (skills/iphone-duo.md).\n\n#### Stop reading the screen and the idiom\n\n| Don't (wrong in resizable / external-display contexts) | Do |\n|---|---|\n| `UIScreen.main` | `window.windowScene?.screen` |\n| `screen.scale` | `traitCollection.displayScale` |\n| `screen.bounds` | the view's own `bounds`, or `windowScene.effectiveGeometry.coordinateSpace.bounds` |\n| `UIDevice.userInterfaceIdiom` for layout | **size classes** (`traitCollection.horizontalSizeClass`) |\n| `supportedInterfaceOrientations` for layout | size classes — orientation is only a *preference* at 27 and is ignored in resizable environments |\n\n`effectiveGeometry` (iOS 16) and the `windowScene(_:didUpdateEffectiveGeometry:)` delegate (iOS 26) are the adaptive-geometry APIs to adopt — they predate 27, but 27 is where ignoring them breaks. `UIRequiresFullScreen` is now honored on iPhone but only enables *discrete* resizing that snaps to orientation-honoring configurations (for games); it no longer fully opts out of resizing.\n\n```swift\noverride func layoutSubviews() {\n super.layoutSubviews()\n let displayScale = traitCollection.displayScale // not UIScreen.main.scale\n // size from self.bounds, not the screen\n}\n\nfunc windowScene(_ windowScene: UIWindowScene,\n didUpdateEffectiveGeometry previous: UIWindowScene.Geometry) {\n let bounds = windowScene.effectiveGeometry.coordinateSpace.bounds\n}\n```\n\n#### Express preferences, not a fixed canvas\n\nYou no longer own a fixed canvas — you express preferences the user and system honor.\n\n- **Minimum size** — the documented replacement for the old `UIRequiresFullScreen` opt-out (TN3192). Set it on the scene's `UISceneSizeRestrictions` so users can't shrink the window below a usable size:\n ```swift\n windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 600)\n ```\n- **Orientation lock** — a *preference*, not a guarantee, in resizable environments. Override `UIViewController.prefersInterfaceOrientationLocked` (returns `Bool`) and call `setNeedsUpdateOfPrefersInterfaceOrientationLocked()` when it changes; read the resolved state from `windowScene.effectiveGeometry.isInterfaceOrientationLocked` (iOS 26).\n- **Requesting an orientation** — iPad refuses it under the windowing model. `windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))` calls its `errorHandler` with `UISceneErrorDomain` code 101, \"The current windowing mode does not allow for programmatic changes to interface orientation\", and the scene keeps its size. The error handler is the only signal, so always pass one. The same request rotated an iPhone scene (measured on iPad Pro 11-inch and iPhone 17, iOS 27.0 simulators). On iPad, lay out for the size you're given instead of forcing an orientation.\n- **Interactive vs settled resize** — `UIWindowSceneGeometry.isInteractivelyResizing` (iOS 26) is `true` while the user drags; throttle expensive work during the drag and settle when it clears. SwiftUI's equivalent is `.onInteractiveResizeChange(_:)` (see axiom-swiftui (skills/layout-ref.md)).\n\n## iPhone Mirroring compatibility\n\nUnder iPhone Mirroring the app keeps running on the iPhone; the Mac supplies indirect mouse/trackpad/keyboard input and, from the 27 cycle, a freely resizable window. Standard UIKit and SwiftUI controls receive translated input correctly — the compatibility work is in custom gestures, biometric auth, and orientation assumptions.\n\n#### The orientation trap\n\nA mirrored app **always reports portrait interface orientation, regardless of the window's aspect ratio**. A landscape-shaped mirrored window is still \"portrait\". Never derive layout from `interfaceOrientation` — use size classes and the scene's `effectiveGeometry` (see the table above).\n\n#### Indirect input reaches custom gestures differently\n\nWith `UIApplicationSupportsIndirectInputEvents` in effect (Info.plist key, iOS 13.4; treated as enabled when built against iOS 17+ SDKs):\n\n- Pointer clicks arrive as `UITouch` of type `.indirectPointer`, not `.direct`.\n- Trackpad pinch and rotate arrive as `UIEvent.EventType.transform` events that drive **only** `UIPinchGestureRecognizer` and `UIRotationGestureRecognizer`. For these events `numberOfTouches` is `0` and `location(ofTouch:in:)` raises — a custom recognizer that reads individual touches must detect non-touch events first (e.g. in `shouldReceive(_:)`).\n- Scroll input is not touch: `allowedScrollTypesMask` (iOS 13.4) controls which scroll types a custom pan recognizer receives. `.discrete` is wheel-mouse scrolling, `.continuous` is trackpad — Mirroring users bring either device, so set `.all` explicitly:\n ```swift\n panRecognizer.allowedScrollTypesMask = .all // wheel mice AND trackpads\n ```\n `UIScrollView` handles both automatically; this is only for custom pan handling.\n\n#### Biometric auth needs a companion path\n\nBiometric requests fail by default under Mirroring — the iPhone's Face ID/Touch ID sensors are not accessible from the Mac (TN3210). Use the companion-capable Local Authentication policy (iOS 18) so people can approve on the Mac (or a paired Apple Watch) instead:\n\n```swift\nlet context = LAContext()\ntry await context.evaluatePolicy(\n .deviceOwnerAuthenticationWithBiometricsOrCompanion,\n localizedReason: \"Unlock your vault\")\n```\n\nWith no companion nearby it behaves exactly like `.deviceOwnerAuthenticationWithBiometrics`, so it is safe as the default policy. On iOS the companion types are the Mac (iOS 18) and Vision Pro (iOS 26) — `LACompanionType` has no Watch case on iOS. A companion-only request with no companion available throws `LAError.companionNotAvailable`. For keychain items gated by `SecAccessControl`, add the `.companion` flag (iOS 18) alongside biometry — see axiom-security (skills/keychain.md).\n\n#### Drag and drop crosses devices\n\nStandard drag interactions (`UIDragInteraction`/`UIDropInteraction`, SwiftUI `Transferable`) participate in iPhone↔Mac drag and drop automatically (iOS 18.1/macOS 15.1). No Mirroring-specific API exists — apps that already implement standard drag and drop get the cross-device behavior for free. See axiom-swift (skills/transferable-ref.md).\n\n#### Test it for real\n\nIterate with `devicectl device appResize` — scriptable, so breakpoints can be swept and asserted automatically (see axiom-tools (skills/device-control-ref.md)) — or by hand in Device Hub's resizable simulator. Then validate in **actual iPhone Mirroring on macOS 27**; Apple's guidance is resizable simulator first, real devices and Mirroring to confirm. TN3210's checklist includes verifying that pinch, rotate, and scroll gestures work with a trackpad or mouse.\n\n## Desktop-class input — pointer, hardware keyboard, Pencil\n\nThe same environments that resize your windows also bring pointers, hardware keyboards, and (on iPad) the Pencil. UIKit's opt-in surface:\n\n- **Pointer effects** — `UIPointerInteraction` (iOS 13.4) gives views the system hover treatment; return a `UIPointerStyle` from the delegate to shape the region. `UIHoverGestureRecognizer` (iOS 13) tracks pointer movement over a view (plus Pencil hover `zOffset` from iOS 16.1).\n- **Key commands** — register `UIKeyCommand`s on responders (or via `UIMenuBuilder`, which doubles as the iPad menu bar). The iPad hold-⌘ shortcut HUD shows each command's `discoverabilityTitle`, falling back to its `title`; a command with neither is invisible to discovery.\n- **Scribble** — Pencil handwriting into text fields is automatic for anything conforming to `UITextInput` (iOS 14, iPad): `UITextField`, `UITextView`, SwiftUI `TextField` all work with zero code. The work is only at the edges: `UIScribbleInteraction` (delegate) disables or tunes writing per view — e.g. suppress it on a field with a custom input view; `UIIndirectScribbleInteraction` makes UI that doesn't *look* like a text field writable (a tap-to-edit title, a canvas that should accept handwritten labels) by vending virtual writable \"elements\". A custom text editor must conform to `UITextInput` to participate at all.\n- SwiftUI equivalents (`onHover`, `hoverEffect`, `keyboardShortcut`) live in axiom-swiftui (skills/gestures.md Pattern 8).\n\n```swift\nbutton.addInteraction(UIPointerInteraction(delegate: self))\n\noverride var keyCommands: [UIKeyCommand]? {\n let find = UIKeyCommand(input: \"f\", modifierFlags: .command,\n action: #selector(focusSearch))\n find.discoverabilityTitle = \"Find\"\n return [find]\n}\n```\n\n## Navigation bar titles and subtitles (iOS 26)\n\n`UINavigationItem` gained a subtitle, attributed titles, and separate content under the large title. Apple's Mail example puts the unread count in `subtitle` and, while filtering, a filter button in `largeSubtitleView`:\n\n```swift\nnavigationItem.title = \"Inbox\"\nnavigationItem.subtitle = \"49 Unread\"\nnavigationItem.largeSubtitleView = filterButton\n```\n\n| Slot | String | Attributed (beats string) | Custom view (beats both) | When all are nil |\n|---|---|---|---|---|\n| Inline title | `title` | `attributedTitle` | `titleView` | no title |\n| Inline subtitle | `subtitle` | `attributedSubtitle` | `subtitleView` | no subtitle |\n| Large title | `largeTitle` | — | — | uses `title` |\n| Large subtitle | `largeSubtitle` | `largeAttributedSubtitle` | `largeSubtitleView` | falls back to the `subtitle` string |\n\nIn Swift the attributed properties are `AttributedString?`. When `titleView` is non-nil, `attributedSubtitle` is ignored too. Everything except `title`/`titleView` is iOS 26 and unavailable on tvOS, watchOS, and visionOS.\n\nVerified on iPhone, iOS 26.5 and 27.0:\n- **`largeSubtitleTextAttributes` is ignored.** A foreground color in it had no effect whether set on the item's appearances, the bar's, or `UINavigationBar.appearance()`; the large subtitle took `subtitleTextAttributes`, even with `largeSubtitle` set. Style it with `largeAttributedSubtitle`, and the inline bar with `attributedSubtitle`; both render their attributes. Prefer attributed strings to appearance objects anyway, since the new design asks apps to drop bar background customization.\n- **`largeSubtitleView` disappears when the bar collapses**, and the inline bar shows `subtitle`. Set `subtitle` too when the status must survive scrolling.\n- **A `UIButton` in `largeSubtitleView` renders centered.** Set `contentHorizontalAlignment = .leading` and zero `contentInsets` in its configuration to align it under the large title.\n- **Custom views win their slot even when empty.** A leftover `UIView()` in `subtitleView` or `largeSubtitleView` silently hides that slot's string. To remove a subtitle, nil every variant.\n- **Subtitles need the new design.** Nothing renders in the compatibility mode `UIDesignRequiresCompatibility` requests, which applies on iOS 26.x and, for apps still built with the 26 SDK, on iOS 27 (axiom-design (skills/liquid-glass.md)). Before 26 or in compatibility mode, compose a two-line `titleView`, and set it only on that path, because a non-nil `titleView` replaces the native title. There is no runtime check for compatibility mode, so decide by build: a 27-SDK app that keeps the key sets `subtitle` only under `#available(iOS 27, *)` and the `titleView` on 26.x; a 26-SDK app with the key uses the `titleView` on every OS.\n\nLarge titles now sit at the top of the scroll view and scroll with the content. Extend the scroll view fully under the navigation bar so the large title stays visible.\n\nSwiftUI peers (`.navigationSubtitle` plus the `.title`, `.subtitle`, `.largeTitle`, and `.largeSubtitle` toolbar placements) are in axiom-swiftui (skills/toolbars.md) Pattern 14.\n\n## New 27 additive APIs\n\n| API | Scope | Use |\n|-----|-------|-----|\n| `UITabBarController.prominentTabIdentifier` | `iOS27`/`visionOS27` | mark one tab always-visible/prominent |\n| `UITabBarControllerSidebar.preferredPlacement` (`.sidebar`) + `Placement` | `iOS27`/`visionOS27` | iPhone can now opt a tab bar into a sidebar (the `sidebar` object itself is iOS 18) |\n| `UINavigationItem.navigationBarMinimization` (`UIBarMinimization`: `minimizationBehavior`/`safeAreaAdjustment`/`restorationBehavior`) | `iOS27` — types also tvOS/visionOS, but see below | control how the nav bar minimizes on scroll; SwiftUI peers are the `toolbarMinimization*` modifiers — see axiom-swiftui (skills/toolbars.md) Pattern 12 |\n| `UIMenuElement.preferredImageVisibility` | `iOS27` | Liquid Glass may hide menu images by default; opt an item back in |\n| `CMMotionManager.deviceMotionBody` | `iOS27`/`watchOS27`/`visionOS27` | assign a `UIView` as the motion reference frame (Body protocols) |\n| `CLLocationManager.headingBody` | `iOS27`/`macOS27`/`watchOS27` | replaces the deprecated `headingOrientation` |\n| `UITraitCollection.systemPrefersReducedResourceUsage` (+ `UITraitSystemPrefersReducedResourceUsage`, `.systemPrefersReducedResourceUsageDidChange`) | `iOS27`/`tvOS27`/`visionOS27` | system asks the app to cut discretionary work under resource pressure — react via `registerForTraitChanges`; see `axiom-performance (energy.md)` for the response playbook |\n\n`UIView` conforms to the CoreMotion/CoreLocation Body protocols, so you set `motionManager.deviceMotionBody = view` / `locationManager.headingBody = view` directly.\n\n#### Nav bar minimization in depth `OS27`\n\nIn Swift `UIBarMinimization` is a **struct** (the ObjC class is refined for Swift), read and written through `UINavigationItem.navigationBarMinimization`:\n\n```swift\nitem.navigationBarMinimization.minimizationBehavior = .onScrollDown\nitem.navigationBarMinimization.restorationBehavior = .atScrollEdge\nitem.navigationBarMinimization.safeAreaAdjustment = .enabled\n```\n\n| Property | Values | Effect |\n|---|---|---|\n| `minimizationBehavior` | `.automatic`, `.never`, `.onScrollDown`, `.onScrollUp` | whether, and in which scroll direction, the bar minimizes |\n| `safeAreaAdjustment` | `.automatic`, `.enabled`, `.disabled` | `.enabled` lets content reflow into the freed space; `.disabled` keeps the safe area fixed |\n| `restorationBehavior` | `.automatic`, `.atScrollEdge` | `.automatic` restores when the user reverses scroll direction; `.atScrollEdge` only when content reaches the scroll edge |\n\n- **`.atScrollEdge` is *currently* honored only alongside `.onScrollDown`.** Apple's wording is \"Currently this is only honored alongside…\", so treat it as present behavior rather than a guarantee. With any other minimization behavior the system silently falls back to `.automatic` — pairing it with `.onScrollUp` is a no-op, not an error.\n- The system already selects `.atScrollEdge` on its own for navigation items whose `preferredSearchBarPlacement` is `.integratedCentered` (iOS 26).\n- Minimizing the navigation bar also minimizes an **integrated top tab bar**. Only the navigation bar supports customizing `safeAreaAdjustment`.\n- **The three types are iOS/tvOS/visionOS 27, but every case except `.automatic` is iOS-only** — explicitly unavailable on tvOS, visionOS and watchOS. A shared helper that assigns `.onScrollDown` fails to compile for tvOS (`'onScrollDown' is unavailable in tvOS`), so gate the assignment itself, not just the property access.\n\n## Apple Intelligence touchpoints\n\nMenus gain an automatic \"Ask Siri\" affordance, and UIKit adds a View Annotations API to annotate views with `AppEntity`s for Siri context (see WWDC 2026-278). If you support drag and drop, Siri may load resources via your drag handlers — avoid animations/modal UI in `sessionWillBegin` (a drag can start without a gesture); put stateful drag UI in `sessionDidMove`.\n\n## Let Xcode do the mechanical migration\n\nXcode 27 ships an app-modernization agent skill that rewrites `UIScreen.main` calls → `traitCollection`/scene bounds, orientation checks → size classes, and can migrate to the scene life cycle. Export the skill for other tools with `xcrun agent skills export`. See `axiom-xcode-mcp` for the agentic-Xcode workflow. Xcode 27.1 extends this skill to SwiftUI and iPhone Duo under a new name — see axiom-swiftui (skills/iphone-duo.md, Tooling and Testing).\n\n## Resources\n\n**WWDC**: 2025-243, 2025-284, 2026-278\n\n**Docs**: /uikit/app-and-environment, /uikit/uiscenedelegate, /uikit/uiwindowscene, /uikit/uiscenesizerestrictions, /uikit/transitioning-to-the-uikit-scene-based-life-cycle, /uikit/uitabbarcontroller, /uikit/uitabbarcontrollersidebar, /uikit/uimenuelement, /technotes/tn3192-migrating-your-app-from-the-deprecated-uirequiresfullscreen-key, /technotes/tn3210-optimizing-your-app-for-iphone-mirroring, /technotes/tn3208-preparing-your-apps-launch-screen-to-meet-app-store-requirements, /bundleresources/information-property-list/uiapplicationscenemanifest, /bundleresources/information-property-list/uilaunchscreen, /bundleresources/information-property-list/uiapplicationsupportsindirectinputevents, /uikit/uipangesturerecognizer/allowedscrolltypesmask, /localauthentication/lapolicy, /uikit/drag-and-drop, /uikit/uiscribbleinteraction, /uikit/uiindirectscribbleinteraction, /uikit/uiresponder/undomanager, /uikit/uibarminimization, /uikit/uinavigationitem/navigationbarminimization, /uikit/uinavigationitem/subtitle, /uikit/uinavigationitem/largesubtitleview, /uikit/customizing-your-app-s-navigation-bar, /uikit/uisceneaccessory, /uikit/uisceneaccessoryregistration\n\n**Skills**: skills/uikit-bridging.md, axiom-xcode-mcp, axiom-swiftui (size-class-driven adaptive layout), axiom-swiftui (skills/toolbars.md), axiom-security (skills/keychain.md), axiom-swift (skills/transferable-ref.md)\n", "skillType": "discipline", "source": "axiom", "tags": [ @@ -43044,56 +43044,56 @@ "heading": "Every app is now resizable", "level": 2, "startLine": 111, - "endLine": 150, - "charCount": 2723 + "endLine": 151, + "charCount": 3312 }, { "heading": "iPhone Mirroring compatibility", "level": 2, - "startLine": 151, - "endLine": 191, + "startLine": 152, + "endLine": 192, "charCount": 3579 }, { "heading": "Desktop-class input — pointer, hardware keyboard, Pencil", "level": 2, - "startLine": 192, - "endLine": 211, + "startLine": 193, + "endLine": 212, "charCount": 1807 }, { "heading": "Navigation bar titles and subtitles (iOS 26)", "level": 2, - "startLine": 212, - "endLine": 241, + "startLine": 213, + "endLine": 242, "charCount": 3130 }, { "heading": "New 27 additive APIs", "level": 2, - "startLine": 242, - "endLine": 276, + "startLine": 243, + "endLine": 277, "charCount": 3511 }, { "heading": "Apple Intelligence touchpoints", "level": 2, - "startLine": 277, - "endLine": 280, + "startLine": 278, + "endLine": 281, "charCount": 401 }, { "heading": "Let Xcode do the mechanical migration", "level": 2, - "startLine": 281, - "endLine": 284, + "startLine": 282, + "endLine": 285, "charCount": 495 }, { "heading": "Resources", "level": 2, - "startLine": 285, - "endLine": 292, + "startLine": 286, + "endLine": 293, "charCount": 1442 } ], @@ -44757,7 +44757,7 @@ "axiom-xcode-mcp--axe-ref": { "name": "axiom-xcode-mcp--axe-ref", "description": "AXe is a CLI tool for interacting with iOS Simulators using Apple's Accessibility APIs and HID functionality. Single binary, no daemon required.", - "content": "\n# AXe Reference (iOS Simulator UI Automation)\n\nAXe is a CLI tool for interacting with iOS Simulators using Apple's Accessibility APIs and HID functionality. Single binary, no daemon required.\n\n> **SimulatorKit load failure**: AXe needs `SimulatorKit.framework`, which Xcode 27 moved to `Contents/SharedFrameworks/`. **AXe 1.8.0 finds it there on its own — no prefix needed.** Older AXe fails with \"…SimulatorKit.framework … does not exist\". Don't assume from the Xcode version: run `xcui doctor`, which now decides by actually running AXe. Only if it reports an `axe_developer_dir` do you prefix direct `axe` calls with `DEVELOPER_DIR=` (e.g. `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer axe tap …`).\n\n## Installation\n\n```bash\nbrew install cameroncooke/axe/axe\n\n# Verify installation\naxe --version\n```\n\n## Critical Best Practice: describe_ui First\n\n**ALWAYS run `describe_ui` before UI interactions.** Never guess coordinates from screenshots.\n\n**Best practice:** Use describe-ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots).\n\n```bash\n# 1. FIRST: Get the UI tree with frame coordinates\naxe describe-ui --udid $UDID\n\n# 2. THEN: Tap by accessibility ID (preferred)\naxe tap --id \"loginButton\" --udid $UDID\n\n# 3. OR: Tap by label\naxe tap --label \"Login\" --udid $UDID\n\n# 4. LAST RESORT: Tap by coordinates from describe-ui output\naxe tap -x 200 -y 400 --udid $UDID\n```\n\n**Priority order for targeting elements:**\n1. `--id` (accessibilityIdentifier) - most stable\n2. `--label` (accessibility label) - stable but may change with localization\n3. `-x -y` coordinates from `describe-ui` - fragile, use only when no identifier\n\n## Core Concept: Accessibility-First\n\n**AXe's key advantage**: Tap elements by accessibility identifier or label, not just coordinates.\n\n```bash\n# Coordinate-based (fragile - breaks with layout changes)\naxe tap -x 200 -y 400 --udid $UDID\n\n# Accessibility-based (stable - survives UI changes)\naxe tap --id \"loginButton\" --udid $UDID\naxe tap --label \"Login\" --udid $UDID\n```\n\n**Always prefer `--id` or `--label` over coordinates.**\n\n## Getting the Simulator UDID\n\nAXe requires the simulator UDID for most commands:\n\n```bash\n# Get booted simulator UDID\nUDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | .udid' | head -1)\n\n# List all simulators\naxe list-simulators\n```\n\n## Touch & Tap Commands\n\n### Tap by Accessibility Identifier (Recommended)\n\n```bash\n# Tap element with accessibilityIdentifier\naxe tap --id \"loginButton\" --udid $UDID\n\n# Tap element with accessibility label\naxe tap --label \"Submit\" --udid $UDID\n```\n\n### Tap by Coordinates\n\n```bash\n# Basic tap\naxe tap -x 200 -y 400 --udid $UDID\n\n# Tap with timing controls\naxe tap -x 200 -y 400 --pre-delay 0.5 --post-delay 0.3 --udid $UDID\n\n# Long press: use touch with --down --up --delay (tap has no hold option)\naxe touch -x 200 -y 400 --down --up --delay 1.0 --udid $UDID\n```\n\n### Low-Level Touch Events\n\n```bash\n# Touch down (finger press)\naxe touch -x 200 -y 400 --down --udid $UDID\n\n# Touch up (finger release)\naxe touch -x 200 -y 400 --up --udid $UDID\n\n# Both in one call (tap)\naxe touch -x 200 -y 400 --down --up --udid $UDID\n\n# Long press (hold duration in seconds)\naxe touch -x 200 -y 400 --down --up --delay 1.0 --udid $UDID\n```\n\n## Swipe & Gesture Commands\n\n### Custom Swipe\n\n```bash\n# Swipe from point A to point B\naxe swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --udid $UDID\n\n# Swipe with duration (slower = more visible)\naxe swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --duration 0.5 --udid $UDID\n```\n\n### Gesture Presets\n\n```bash\n# Scrolling\naxe gesture scroll-up --udid $UDID # Scroll content up (swipe down)\naxe gesture scroll-down --udid $UDID # Scroll content down (swipe up)\naxe gesture scroll-left --udid $UDID\naxe gesture scroll-right --udid $UDID\n\n# Edge swipes (navigation)\naxe gesture swipe-from-left-edge --udid $UDID # Back navigation\naxe gesture swipe-from-right-edge --udid $UDID\naxe gesture swipe-from-top-edge --udid $UDID # Notification Center\naxe gesture swipe-from-bottom-edge --udid $UDID # Home indicator/Control Center\n```\n\n## Text Input\n\n### Type Text\n\n```bash\n# Type text (element must be focused)\naxe type \"user@example.com\" --udid $UDID\n\n# Type a longer string\naxe type \"password123\" --udid $UDID\n\n# Type from stdin\necho \"Hello World\" | axe type --stdin --udid $UDID\n\n# Type from file\naxe type --file /tmp/input.txt --udid $UDID\n```\n\n### Keyboard Keys\n\n```bash\n# Press specific key by HID keycode\naxe key 40 --udid $UDID # Return/Enter\n\n# Common keycodes:\n# 40 = Return/Enter\n# 41 = Escape\n# 42 = Backspace/Delete\n# 43 = Tab\n# 44 = Space\n# 79 = Right Arrow\n# 80 = Left Arrow\n# 81 = Down Arrow\n# 82 = Up Arrow\n\n# Key sequence with timing (comma-separated keycodes, required --keycodes label)\naxe key-sequence --keycodes 40,43,40 --delay 0.2 --udid $UDID\n```\n\n## Hardware Buttons\n\n```bash\n# Home button\naxe button home --udid $UDID\n\n# Lock/Power button\naxe button lock --udid $UDID\n\n# Long press power (shutdown dialog)\naxe button lock --duration 3.0 --udid $UDID\n\n# Side button (iPhone X+)\naxe button side-button --udid $UDID\n\n# Siri\naxe button siri --udid $UDID\n\n# Apple Pay\naxe button apple-pay --udid $UDID\n```\n\n## Screenshots\n\n```bash\n# Screenshot to auto-named file\naxe screenshot --udid $UDID\n# Output: screenshot_2026-01-11_143052.png\n\n# Screenshot to specific file\naxe screenshot --output /tmp/my-screenshot.png --udid $UDID\n\n# For piping, write to a file then redirect (no --stdout flag exists)\naxe screenshot --output /tmp/shot.png --udid $UDID && cat /tmp/shot.png > screenshot.png\n```\n\n## Video Recording & Streaming\n\n### Record Video\n\n```bash\n# Start recording (Ctrl+C to stop)\naxe record-video --output /tmp/recording.mp4 --udid $UDID\n\n# Record with quality settings\naxe record-video --output /tmp/recording.mp4 --quality high --udid $UDID\n\n# Record with scale (reduce file size)\naxe record-video --output /tmp/recording.mp4 --scale 0.5 --udid $UDID\n```\n\n### Stream Video\n\n```bash\n# Stream at 10 FPS (default)\naxe stream-video --udid $UDID\n\n# Stream at specific framerate (1-30 FPS)\naxe stream-video --fps 30 --udid $UDID\n\n# Stream formats\naxe stream-video --format mjpeg --udid $UDID # MJPEG (default)\naxe stream-video --format jpeg --udid $UDID # Individual JPEGs\naxe stream-video --format ffmpeg --udid $UDID # FFmpeg compatible\naxe stream-video --format bgra --udid $UDID # Raw BGRA\n```\n\n## UI Inspection (describe-ui)\n\n**Critical for finding accessibility identifiers and labels.**\n\n### Full Screen UI Tree\n\n```bash\n# Get complete accessibility tree\naxe describe-ui --udid $UDID\n\n# Output includes:\n# - Element type (Button, TextField, StaticText, etc.)\n# - Accessibility identifier\n# - Accessibility label\n# - Frame (position and size)\n# - Enabled/disabled state\n```\n\n### Point-Specific UI Info\n\n```bash\n# Get element at specific coordinates\naxe describe-ui --point 200,400 --udid $UDID\n```\n\n### Example Output\n\n```json\n{\n \"type\": \"Button\",\n \"identifier\": \"loginButton\",\n \"label\": \"Login\",\n \"frame\": {\"x\": 150, \"y\": 380, \"width\": 100, \"height\": 44},\n \"enabled\": true,\n \"focused\": false\n}\n```\n\n## Common Workflows\n\n### Login Flow\n\n```bash\nUDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | .udid' | head -1)\n\n# Tap email field and type\naxe tap --id \"emailTextField\" --udid $UDID\naxe type \"user@example.com\" --udid $UDID\n\n# Tap password field and type\naxe tap --id \"passwordTextField\" --udid $UDID\naxe type \"password123\" --udid $UDID\n\n# Tap login button\naxe tap --id \"loginButton\" --udid $UDID\n\n# Wait and screenshot\nsleep 2\naxe screenshot --output /tmp/login-result.png --udid $UDID\n```\n\n### Discover Elements Before Automating\n\n```bash\n# 1. Get the UI tree\naxe describe-ui --udid $UDID > /tmp/ui-tree.json\n\n# 2. Find elements (search for identifiers)\ncat /tmp/ui-tree.json | jq '.[] | select(.identifier != null) | {identifier, label, type}'\n\n# 3. Use discovered identifiers in automation\naxe tap --id \"discoveredIdentifier\" --udid $UDID\n```\n\n### Scroll to Find Element\n\n```bash\n# Scroll down until element appears (pseudo-code pattern)\nfor i in {1..5}; do\n if axe describe-ui --udid $UDID | grep -q \"targetElement\"; then\n axe tap --id \"targetElement\" --udid $UDID\n break\n fi\n axe gesture scroll-down --udid $UDID\n sleep 0.5\ndone\n```\n\n### Screenshot on Error\n\n```bash\n# Automation with error capture\nif ! axe tap --id \"submitButton\" --udid $UDID; then\n axe screenshot --output /tmp/error-state.png --udid $UDID\n axe describe-ui --udid $UDID > /tmp/error-ui-tree.json\n echo \"Failed to tap submitButton - see error-state.png\"\nfi\n```\n\n## Timing Controls\n\nMost commands support timing options:\n\n| Option | Description |\n|--------|-------------|\n| `--pre-delay` | Wait before action (seconds, `tap`) |\n| `--post-delay` | Wait after action (seconds, `tap`) |\n| `--delay` | Hold duration between down/up (`touch`) or between key presses (`key-sequence`) |\n| `--duration` | Button-press duration (`button`) or swipe duration (`swipe`) |\n\n```bash\n# Example with full timing control\naxe tap --id \"button\" --pre-delay 0.5 --post-delay 0.3 --udid $UDID\n```\n\n## AXe vs simctl\n\n| Capability | simctl | AXe |\n|------------|--------|-----|\n| Device lifecycle | ✅ | ❌ |\n| Permissions | ✅ | ❌ |\n| Push notifications | ✅ | ❌ |\n| Status bar | ✅ | ❌ |\n| Deep links | ✅ | ❌ |\n| Screenshots | ✅ | ✅ (PNG) |\n| Video recording | ✅ | ✅ (H.264) |\n| Video streaming | ❌ | ✅ |\n| UI tap/swipe | ❌ | ✅ |\n| Type text | ❌ | ✅ |\n| Hardware buttons | ❌ | ✅ |\n| Accessibility tree | ❌ | ✅ |\n\n**Use both together**: simctl for device control, AXe for UI automation.\n\n## Troubleshooting\n\n### Element Not Found\n\n1. Run `axe describe-ui` to see available elements\n2. Check element has `accessibilityIdentifier` set in code\n3. Ensure element is visible (not off-screen)\n\n### Tap Doesn't Work\n\n1. Check element is enabled (`\"enabled\": true` in describe-ui)\n2. Try adding `--pre-delay 0.5` for slow-loading UI\n3. Verify correct UDID with `axe list-simulators`\n\n### Type Not Working\n\n1. Ensure text field is focused first: `axe tap --id \"textField\"`\n2. Check keyboard is visible\n3. For unreliable input, split long strings into multiple `axe type` calls, or use `--stdin`/`--file`\n\n### Permission Denied\n\nAXe uses private APIs - ensure you're running on a Mac with Xcode installed and proper entitlements.\n\n## Resources\n\n**GitHub**: https://github.com/cameroncooke/AXe\n\n**Related**: xcsentinel (build orchestration)\n\n**Skills**: axiom-testing\n\n**Agents**: simulator-tester, test-runner\n", + "content": "\n# AXe Reference (iOS Simulator UI Automation)\n\nAXe is a CLI tool for interacting with iOS Simulators using Apple's Accessibility APIs and HID functionality. Single binary, no daemon required.\n\n> **SimulatorKit load failure**: AXe needs `SimulatorKit.framework`, which Xcode 27 moved to `Contents/SharedFrameworks/`. **AXe 1.8.0 finds it there on its own — no prefix needed.** Older AXe fails with \"…SimulatorKit.framework … does not exist\". Don't assume from the Xcode version: run `xcui doctor`, which now decides by actually running AXe. Only if it reports an `axe_developer_dir` do you prefix direct `axe` calls with `DEVELOPER_DIR=` (e.g. `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer axe tap …`).\n\n## Installation\n\n```bash\nbrew install cameroncooke/axe/axe\n\n# Verify installation\naxe --version\n```\n\n## Critical Best Practice: describe_ui First\n\n**ALWAYS run `describe_ui` before UI interactions.** Never guess coordinates from screenshots.\n\n**Best practice:** Use describe-ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots).\n\n```bash\n# 1. FIRST: Get the UI tree with frame coordinates\naxe describe-ui --udid $UDID\n\n# 2. THEN: Tap by accessibility ID (preferred)\naxe tap --id \"loginButton\" --tap-style physical --udid $UDID\n\n# 3. OR: Tap by label\naxe tap --label \"Login\" --tap-style physical --udid $UDID\n\n# 4. LAST RESORT: Tap by coordinates from describe-ui output\naxe tap -x 200 -y 400 --tap-style physical --udid $UDID\n```\n\n**Always pass `--tap-style physical`.** AXe's default style (`automatic`) sends FBSimulator `tapAt` to anything that isn't a switch, and on Xcode 27.1 + AXe 1.8.0 that activated no SwiftUI `Button`, `List` row, `Menu`, `Menu` row, or tab — while printing `✓ … completed successfully`. `physical` (touch down/up) activated all of them (measured 2026-09-19; matrix in `axiom-tools (skills/xcui-ref.md)`). `xcui tap` adds the flag for you.\n\n**Priority order for targeting elements:**\n1. `--id` (accessibilityIdentifier) - most stable\n2. `--label` (accessibility label) - stable but may change with localization\n3. `-x -y` coordinates from `describe-ui` - fragile, use only when no identifier\n\n## Core Concept: Accessibility-First\n\n**AXe's key advantage**: Tap elements by accessibility identifier or label, not just coordinates.\n\n```bash\n# Coordinate-based (fragile - breaks with layout changes)\naxe tap -x 200 -y 400 --tap-style physical --udid $UDID\n\n# Accessibility-based (stable - survives UI changes)\naxe tap --id \"loginButton\" --tap-style physical --udid $UDID\naxe tap --label \"Login\" --tap-style physical --udid $UDID\n```\n\n**Always prefer `--id` or `--label` over coordinates** — with one caveat: selectors tap the element's accessibility activation point, not its pixels. A view whose accessibility frame is skewed (a label hidden with `.fixedSize()` + `.frame(width: 0).clipped()`) gets tapped off target and the tap still prints ✓. When a selector tap changes nothing, screenshot, measure the visible center, and tap `-x/-y`.\n\n## Getting the Simulator UDID\n\nAXe requires the simulator UDID for most commands:\n\n```bash\n# List booted simulators, then pick one by name — never `head -1`\nxcrun simctl list devices booted\nUDID=\n\n# List all simulators\naxe list-simulators\n```\n\nWith more than one simulator booted, a `head -1` pick drives whichever device sorts first, and every tap on it still prints ✓. `xcui` refuses to guess in that case; direct AXe calls need you to choose.\n\n## Touch & Tap Commands\n\n### Tap by Accessibility Identifier (Recommended)\n\n```bash\n# Tap element with accessibilityIdentifier\naxe tap --id \"loginButton\" --tap-style physical --udid $UDID\n\n# Tap element with accessibility label\naxe tap --label \"Submit\" --tap-style physical --udid $UDID\n```\n\n### Tap by Coordinates\n\n```bash\n# Basic tap\naxe tap -x 200 -y 400 --tap-style physical --udid $UDID\n\n# Tap with timing controls\naxe tap -x 200 -y 400 --tap-style physical --pre-delay 0.5 --post-delay 0.3 --udid $UDID\n\n# Long press: use touch with --down --up --delay (tap has no hold option);\n# a 1.2 s hold opened a SwiftUI .contextMenu\naxe touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID\n```\n\n### Low-Level Touch Events\n\n```bash\n# Touch down (finger press)\naxe touch -x 200 -y 400 --down --udid $UDID\n\n# Touch up (finger release)\naxe touch -x 200 -y 400 --up --udid $UDID\n\n# Both in one call (tap)\naxe touch -x 200 -y 400 --down --up --udid $UDID\n\n# Long press (hold duration in seconds)\naxe touch -x 200 -y 400 --down --up --delay 1.0 --udid $UDID\n```\n\n## Swipe & Gesture Commands\n\n### Custom Swipe\n\n```bash\n# Swipe from point A to point B\naxe swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --udid $UDID\n\n# Swipe with duration (slower = more visible)\naxe swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --duration 0.5 --udid $UDID\n```\n\n### Gesture Presets\n\n```bash\n# Scrolling\naxe gesture scroll-up --udid $UDID # Scroll content up (swipe down)\naxe gesture scroll-down --udid $UDID # Scroll content down (swipe up)\naxe gesture scroll-left --udid $UDID\naxe gesture scroll-right --udid $UDID\n\n# Edge swipes (navigation)\naxe gesture swipe-from-left-edge --udid $UDID # Back navigation\naxe gesture swipe-from-right-edge --udid $UDID\naxe gesture swipe-from-top-edge --udid $UDID # Notification Center\naxe gesture swipe-from-bottom-edge --udid $UDID # Home indicator/Control Center\n```\n\n## Text Input\n\n### Type Text\n\n```bash\n# Type text (element must be focused)\naxe type \"user@example.com\" --udid $UDID\n\n# Type a longer string\naxe type \"password123\" --udid $UDID\n\n# Type from stdin\necho \"Hello World\" | axe type --stdin --udid $UDID\n\n# Type from file\naxe type --file /tmp/input.txt --udid $UDID\n```\n\n### Keyboard Keys\n\n```bash\n# Press specific key by HID keycode\naxe key 40 --udid $UDID # Return/Enter\n\n# Common keycodes:\n# 40 = Return/Enter\n# 41 = Escape\n# 42 = Backspace/Delete\n# 43 = Tab\n# 44 = Space\n# 79 = Right Arrow\n# 80 = Left Arrow\n# 81 = Down Arrow\n# 82 = Up Arrow\n\n# Key sequence with timing (comma-separated keycodes, required --keycodes label)\naxe key-sequence --keycodes 40,43,40 --delay 0.2 --udid $UDID\n```\n\n## Hardware Buttons\n\n```bash\n# Home button\naxe button home --udid $UDID\n\n# Lock/Power button\naxe button lock --udid $UDID\n\n# Long press power (shutdown dialog)\naxe button lock --duration 3.0 --udid $UDID\n\n# Side button (iPhone X+)\naxe button side-button --udid $UDID\n\n# Siri\naxe button siri --udid $UDID\n\n# Apple Pay\naxe button apple-pay --udid $UDID\n```\n\n## Screenshots\n\n```bash\n# Screenshot to auto-named file\naxe screenshot --udid $UDID\n# Output: screenshot_2026-01-11_143052.png\n\n# Screenshot to specific file\naxe screenshot --output /tmp/my-screenshot.png --udid $UDID\n\n# For piping, write to a file then redirect (no --stdout flag exists)\naxe screenshot --output /tmp/shot.png --udid $UDID && cat /tmp/shot.png > screenshot.png\n```\n\n## Video Recording & Streaming\n\n### Record Video\n\n```bash\n# Start recording (Ctrl+C to stop)\naxe record-video --output /tmp/recording.mp4 --udid $UDID\n\n# Record with quality settings\naxe record-video --output /tmp/recording.mp4 --quality high --udid $UDID\n\n# Record with scale (reduce file size)\naxe record-video --output /tmp/recording.mp4 --scale 0.5 --udid $UDID\n```\n\n### Stream Video\n\n```bash\n# Stream at 10 FPS (default)\naxe stream-video --udid $UDID\n\n# Stream at specific framerate (1-30 FPS)\naxe stream-video --fps 30 --udid $UDID\n\n# Stream formats\naxe stream-video --format mjpeg --udid $UDID # MJPEG (default)\naxe stream-video --format jpeg --udid $UDID # Individual JPEGs\naxe stream-video --format ffmpeg --udid $UDID # FFmpeg compatible\naxe stream-video --format bgra --udid $UDID # Raw BGRA\n```\n\n## UI Inspection (describe-ui)\n\n**Critical for finding accessibility identifiers and labels.**\n\n### Full Screen UI Tree\n\n```bash\n# Get complete accessibility tree\naxe describe-ui --udid $UDID\n\n# Output includes:\n# - Element type (Button, TextField, StaticText, etc.)\n# - Accessibility identifier\n# - Accessibility label\n# - Frame (position and size)\n# - Enabled/disabled state\n```\n\n### Point-Specific UI Info\n\n```bash\n# Get element at specific coordinates\naxe describe-ui --point 200,400 --udid $UDID\n```\n\n### Example Output\n\n```json\n{\n \"type\": \"Button\",\n \"identifier\": \"loginButton\",\n \"label\": \"Login\",\n \"frame\": {\"x\": 150, \"y\": 380, \"width\": 100, \"height\": 44},\n \"enabled\": true,\n \"focused\": false\n}\n```\n\n## Common Workflows\n\n### Login Flow\n\n```bash\nUDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | .udid' | head -1)\n\n# Tap email field and type\naxe tap --id \"emailTextField\" --tap-style physical --udid $UDID\naxe type \"user@example.com\" --udid $UDID\n\n# Tap password field and type\naxe tap --id \"passwordTextField\" --tap-style physical --udid $UDID\naxe type \"password123\" --udid $UDID\n\n# Tap login button\naxe tap --id \"loginButton\" --tap-style physical --udid $UDID\n\n# Wait and screenshot\nsleep 2\naxe screenshot --output /tmp/login-result.png --udid $UDID\n```\n\n### Discover Elements Before Automating\n\n```bash\n# 1. Get the UI tree\naxe describe-ui --udid $UDID > /tmp/ui-tree.json\n\n# 2. Find elements (search for identifiers)\ncat /tmp/ui-tree.json | jq '.[] | select(.identifier != null) | {identifier, label, type}'\n\n# 3. Use discovered identifiers in automation\naxe tap --id \"discoveredIdentifier\" --tap-style physical --udid $UDID\n```\n\n### Scroll to Find Element\n\n```bash\n# Scroll down until element appears (pseudo-code pattern)\nfor i in {1..5}; do\n if axe describe-ui --udid $UDID | grep -q \"targetElement\"; then\n axe tap --id \"targetElement\" --tap-style physical --udid $UDID\n break\n fi\n axe gesture scroll-down --udid $UDID\n sleep 0.5\ndone\n```\n\n### Screenshot on Error\n\n```bash\n# Automation with error capture\nif ! axe tap --id \"submitButton\" --tap-style physical --udid $UDID; then\n axe screenshot --output /tmp/error-state.png --udid $UDID\n axe describe-ui --udid $UDID > /tmp/error-ui-tree.json\n echo \"Failed to tap submitButton - see error-state.png\"\nfi\n```\n\n## Timing Controls\n\nMost commands support timing options:\n\n| Option | Description |\n|--------|-------------|\n| `--pre-delay` | Wait before action (seconds, `tap`) |\n| `--post-delay` | Wait after action (seconds, `tap`) |\n| `--delay` | Hold duration between down/up (`touch`) or between key presses (`key-sequence`) |\n| `--duration` | Button-press duration (`button`) or swipe duration (`swipe`) |\n\n```bash\n# Example with full timing control\naxe tap --id \"button\" --pre-delay 0.5 --post-delay 0.3 --tap-style physical --udid $UDID\n```\n\n## AXe vs simctl\n\n| Capability | simctl | AXe |\n|------------|--------|-----|\n| Device lifecycle | ✅ | ❌ |\n| Permissions | ✅ | ❌ |\n| Push notifications | ✅ | ❌ |\n| Status bar | ✅ | ❌ |\n| Deep links | ✅ | ❌ |\n| Screenshots | ✅ | ✅ (PNG) |\n| Video recording | ✅ | ✅ (H.264) |\n| Video streaming | ❌ | ✅ |\n| UI tap/swipe | ❌ | ✅ |\n| Type text | ❌ | ✅ |\n| Hardware buttons | ❌ | ✅ |\n| Accessibility tree | ❌ | ✅ |\n\n**Use both together**: simctl for device control, AXe for UI automation.\n\n## Troubleshooting\n\n### Element Not Found\n\n1. Run `axe describe-ui` to see available elements\n2. Check element has `accessibilityIdentifier` set in code\n3. Ensure element is visible (not off-screen)\n\n### Tap Doesn't Work\n\nA tap that changes nothing still prints `✓ … completed successfully`, so check in this order:\n\n1. **Tap style** — add `--tap-style physical`. The default sends `tapAt`, which SwiftUI controls ignored in every case measured.\n2. **Wrong device** — with more than one simulator booted, confirm `--udid` names the one on screen (`xcrun simctl list devices booted`).\n3. **Skewed accessibility frame** — the selector resolved, but to a point off the visible control; tap measured `-x/-y` instead.\n4. Check element is enabled (`\"enabled\": true` in describe-ui)\n5. Try adding `--pre-delay 0.5` for slow-loading UI\n\n### Type Not Working\n\n1. Ensure text field is focused first: `axe tap --id \"textField\" --tap-style physical`\n2. Check keyboard is visible\n3. For unreliable input, split long strings into multiple `axe type` calls, or use `--stdin`/`--file`\n\n### Permission Denied\n\nAXe uses private APIs - ensure you're running on a Mac with Xcode installed and proper entitlements.\n\n## Resources\n\n**GitHub**: https://github.com/cameroncooke/AXe\n\n**Related**: xcsentinel (build orchestration)\n\n**Skills**: axiom-testing\n\n**Agents**: simulator-tester, test-runner\n", "skillType": "reference", "source": "axiom", "tags": [ @@ -44824,587 +44824,594 @@ "level": 1, "startLine": 26, "endLine": 28, - "charCount": 87 + "charCount": 108 }, { "heading": "3. OR: Tap by label", "level": 1, "startLine": 29, "endLine": 31, - "charCount": 59 + "charCount": 80 }, { "heading": "4. LAST RESORT: Tap by coordinates from describe-ui output", "level": 1, "startLine": 32, - "endLine": 40, - "charCount": 352 + "endLine": 42, + "charCount": 811 }, { "heading": "Core Concept: Accessibility-First", "level": 2, - "startLine": 41, - "endLine": 45, + "startLine": 43, + "endLine": 47, "charCount": 144 }, { "heading": "Coordinate-based (fragile - breaks with layout changes)", "level": 1, - "startLine": 46, - "endLine": 48, - "charCount": 93 + "startLine": 48, + "endLine": 50, + "charCount": 114 }, { "heading": "Accessibility-based (stable - survives UI changes)", "level": 1, - "startLine": 49, - "endLine": 55, - "charCount": 191 + "startLine": 51, + "endLine": 57, + "charCount": 583 }, { "heading": "Getting the Simulator UDID", "level": 2, - "startLine": 56, - "endLine": 60, + "startLine": 58, + "endLine": 62, "charCount": 90 }, { - "heading": "Get booted simulator UDID", + "heading": "List booted simulators, then pick one by name — never `head -1`", "level": 1, - "startLine": 61, - "endLine": 63, - "charCount": 157 + "startLine": 63, + "endLine": 66, + "charCount": 134 }, { "heading": "List all simulators", "level": 1, - "startLine": 64, - "endLine": 67, - "charCount": 46 + "startLine": 67, + "endLine": 72, + "charCount": 249 }, { "heading": "Touch & Tap Commands", "level": 2, - "startLine": 68, - "endLine": 72, + "startLine": 73, + "endLine": 77, "charCount": 83 }, { "heading": "Tap element with accessibilityIdentifier", "level": 1, - "startLine": 73, - "endLine": 75, - "charCount": 83 + "startLine": 78, + "endLine": 80, + "charCount": 104 }, { "heading": "Tap element with accessibility label", "level": 1, - "startLine": 76, - "endLine": 82, - "charCount": 113 + "startLine": 81, + "endLine": 87, + "charCount": 134 }, { "heading": "Basic tap", "level": 1, - "startLine": 83, - "endLine": 85, - "charCount": 47 + "startLine": 88, + "endLine": 90, + "charCount": 68 }, { "heading": "Tap with timing controls", "level": 1, - "startLine": 86, - "endLine": 88, - "charCount": 95 + "startLine": 91, + "endLine": 93, + "charCount": 116 }, { - "heading": "Long press: use touch with --down --up --delay (tap has no hold option)", + "heading": "Long press: use touch with --down --up --delay (tap has no hold option);", "level": 1, - "startLine": 89, - "endLine": 95, - "charCount": 175 + "startLine": 94, + "endLine": 94, + "charCount": 74 + }, + { + "heading": "a 1.2 s hold opened a SwiftUI .contextMenu", + "level": 1, + "startLine": 95, + "endLine": 101, + "charCount": 146 }, { "heading": "Touch down (finger press)", "level": 1, - "startLine": 96, - "endLine": 98, + "startLine": 102, + "endLine": 104, "charCount": 72 }, { "heading": "Touch up (finger release)", "level": 1, - "startLine": 99, - "endLine": 101, + "startLine": 105, + "endLine": 107, "charCount": 70 }, { "heading": "Both in one call (tap)", "level": 1, - "startLine": 102, - "endLine": 104, + "startLine": 108, + "endLine": 110, "charCount": 74 }, { "heading": "Long press (hold duration in seconds)", "level": 1, - "startLine": 105, - "endLine": 108, + "startLine": 111, + "endLine": 114, "charCount": 105 }, { "heading": "Swipe & Gesture Commands", "level": 2, - "startLine": 109, - "endLine": 113, + "startLine": 115, + "endLine": 119, "charCount": 54 }, { "heading": "Swipe from point A to point B", "level": 1, - "startLine": 114, - "endLine": 116, + "startLine": 120, + "endLine": 122, "charCount": 107 }, { "heading": "Swipe with duration (slower = more visible)", "level": 1, - "startLine": 117, - "endLine": 123, + "startLine": 123, + "endLine": 129, "charCount": 169 }, { "heading": "Scrolling", "level": 1, - "startLine": 124, - "endLine": 129, + "startLine": 130, + "endLine": 135, "charCount": 233 }, { "heading": "Edge swipes (navigation)", "level": 1, - "startLine": 130, - "endLine": 136, + "startLine": 136, + "endLine": 142, "charCount": 294 }, { "heading": "Text Input", "level": 2, - "startLine": 137, - "endLine": 141, + "startLine": 143, + "endLine": 147, "charCount": 37 }, { "heading": "Type text (element must be focused)", "level": 1, - "startLine": 142, - "endLine": 144, + "startLine": 148, + "endLine": 150, "charCount": 79 }, { "heading": "Type a longer string", "level": 1, - "startLine": 145, - "endLine": 147, + "startLine": 151, + "endLine": 153, "charCount": 59 }, { "heading": "Type from stdin", "level": 1, - "startLine": 148, - "endLine": 150, + "startLine": 154, + "endLine": 156, "charCount": 69 }, { "heading": "Type from file", "level": 1, - "startLine": 151, - "endLine": 157, + "startLine": 157, + "endLine": 163, "charCount": 92 }, { "heading": "Press specific key by HID keycode", "level": 1, - "startLine": 158, - "endLine": 160, + "startLine": 164, + "endLine": 166, "charCount": 76 }, { "heading": "Common keycodes:", "level": 1, - "startLine": 161, - "endLine": 161, - "charCount": 18 - }, - { - "heading": "40 = Return/Enter", - "level": 1, - "startLine": 162, - "endLine": 162, - "charCount": 19 - }, - { - "heading": "41 = Escape", - "level": 1, - "startLine": 163, - "endLine": 163, - "charCount": 13 - }, - { - "heading": "42 = Backspace/Delete", - "level": 1, - "startLine": 164, - "endLine": 164, - "charCount": 23 - }, - { - "heading": "43 = Tab", - "level": 1, - "startLine": 165, - "endLine": 165, - "charCount": 10 - }, - { - "heading": "44 = Space", - "level": 1, - "startLine": 166, - "endLine": 166, - "charCount": 12 - }, - { - "heading": "79 = Right Arrow", - "level": 1, "startLine": 167, "endLine": 167, "charCount": 18 }, { - "heading": "80 = Left Arrow", + "heading": "40 = Return/Enter", "level": 1, "startLine": 168, "endLine": 168, + "charCount": 19 + }, + { + "heading": "41 = Escape", + "level": 1, + "startLine": 169, + "endLine": 169, + "charCount": 13 + }, + { + "heading": "42 = Backspace/Delete", + "level": 1, + "startLine": 170, + "endLine": 170, + "charCount": 23 + }, + { + "heading": "43 = Tab", + "level": 1, + "startLine": 171, + "endLine": 171, + "charCount": 10 + }, + { + "heading": "44 = Space", + "level": 1, + "startLine": 172, + "endLine": 172, + "charCount": 12 + }, + { + "heading": "79 = Right Arrow", + "level": 1, + "startLine": 173, + "endLine": 173, + "charCount": 18 + }, + { + "heading": "80 = Left Arrow", + "level": 1, + "startLine": 174, + "endLine": 174, "charCount": 17 }, { "heading": "81 = Down Arrow", "level": 1, - "startLine": 169, - "endLine": 169, + "startLine": 175, + "endLine": 175, "charCount": 17 }, { "heading": "82 = Up Arrow", "level": 1, - "startLine": 170, - "endLine": 171, + "startLine": 176, + "endLine": 177, "charCount": 16 }, { "heading": "Key sequence with timing (comma-separated keycodes, required --keycodes label)", "level": 1, - "startLine": 172, - "endLine": 175, + "startLine": 178, + "endLine": 181, "charCount": 147 }, { "heading": "Hardware Buttons", "level": 2, - "startLine": 176, - "endLine": 178, + "startLine": 182, + "endLine": 184, "charCount": 28 }, { "heading": "Home button", "level": 1, - "startLine": 179, - "endLine": 181, + "startLine": 185, + "endLine": 187, "charCount": 43 }, { "heading": "Lock/Power button", "level": 1, - "startLine": 182, - "endLine": 184, + "startLine": 188, + "endLine": 190, "charCount": 49 }, { "heading": "Long press power (shutdown dialog)", "level": 1, - "startLine": 185, - "endLine": 187, + "startLine": 191, + "endLine": 193, "charCount": 81 }, { "heading": "Side button (iPhone X+)", "level": 1, - "startLine": 188, - "endLine": 190, + "startLine": 194, + "endLine": 196, "charCount": 62 }, { "heading": "Siri", "level": 1, - "startLine": 191, - "endLine": 193, + "startLine": 197, + "endLine": 199, "charCount": 36 }, { "heading": "Apple Pay", "level": 1, - "startLine": 194, - "endLine": 197, + "startLine": 200, + "endLine": 203, "charCount": 50 }, { "heading": "Screenshots", "level": 2, - "startLine": 198, - "endLine": 200, + "startLine": 204, + "endLine": 206, "charCount": 23 }, { "heading": "Screenshot to auto-named file", "level": 1, - "startLine": 201, - "endLine": 202, + "startLine": 207, + "endLine": 208, "charCount": 59 }, { "heading": "Output: screenshot_2026-01-11_143052.png", "level": 1, - "startLine": 203, - "endLine": 204, + "startLine": 209, + "endLine": 210, "charCount": 43 }, { "heading": "Screenshot to specific file", "level": 1, - "startLine": 205, - "endLine": 207, + "startLine": 211, + "endLine": 213, "charCount": 90 }, { "heading": "For piping, write to a file then redirect (no --stdout flag exists)", "level": 1, - "startLine": 208, - "endLine": 211, + "startLine": 214, + "endLine": 217, "charCount": 163 }, { "heading": "Video Recording & Streaming", "level": 2, - "startLine": 212, - "endLine": 216, + "startLine": 218, + "endLine": 222, "charCount": 57 }, { "heading": "Start recording (Ctrl+C to stop)", "level": 1, - "startLine": 217, - "endLine": 219, + "startLine": 223, + "endLine": 225, "charCount": 93 }, { "heading": "Record with quality settings", "level": 1, - "startLine": 220, - "endLine": 222, + "startLine": 226, + "endLine": 228, "charCount": 104 }, { "heading": "Record with scale (reduce file size)", "level": 1, - "startLine": 223, - "endLine": 229, + "startLine": 229, + "endLine": 235, "charCount": 139 }, { "heading": "Stream at 10 FPS (default)", "level": 1, - "startLine": 230, - "endLine": 232, + "startLine": 236, + "endLine": 238, "charCount": 59 }, { "heading": "Stream at specific framerate (1-30 FPS)", "level": 1, - "startLine": 233, - "endLine": 235, + "startLine": 239, + "endLine": 241, "charCount": 81 }, { "heading": "Stream formats", "level": 1, - "startLine": 236, - "endLine": 242, + "startLine": 242, + "endLine": 248, "charCount": 277 }, { "heading": "UI Inspection (describe-ui)", "level": 2, - "startLine": 243, - "endLine": 249, + "startLine": 249, + "endLine": 255, "charCount": 128 }, { "heading": "Get complete accessibility tree", "level": 1, - "startLine": 250, - "endLine": 252, + "startLine": 256, + "endLine": 258, "charCount": 63 }, { "heading": "Output includes:", "level": 1, - "startLine": 253, - "endLine": 253, + "startLine": 259, + "endLine": 259, "charCount": 18 }, { "heading": "- Element type (Button, TextField, StaticText, etc.)", "level": 1, - "startLine": 254, - "endLine": 254, + "startLine": 260, + "endLine": 260, "charCount": 54 }, { "heading": "- Accessibility identifier", "level": 1, - "startLine": 255, - "endLine": 255, + "startLine": 261, + "endLine": 261, "charCount": 28 }, { "heading": "- Accessibility label", "level": 1, - "startLine": 256, - "endLine": 256, + "startLine": 262, + "endLine": 262, "charCount": 23 }, { "heading": "- Frame (position and size)", "level": 1, - "startLine": 257, - "endLine": 257, + "startLine": 263, + "endLine": 263, "charCount": 29 }, { "heading": "- Enabled/disabled state", "level": 1, - "startLine": 258, - "endLine": 263, + "startLine": 264, + "endLine": 269, "charCount": 67 }, { "heading": "Get element at specific coordinates", "level": 1, - "startLine": 264, - "endLine": 280, + "startLine": 270, + "endLine": 286, "charCount": 294 }, { "heading": "Common Workflows", "level": 2, - "startLine": 281, - "endLine": 287, + "startLine": 287, + "endLine": 293, "charCount": 174 }, { "heading": "Tap email field and type", "level": 1, - "startLine": 288, - "endLine": 291, - "charCount": 111 + "startLine": 294, + "endLine": 297, + "charCount": 132 }, { "heading": "Tap password field and type", "level": 1, - "startLine": 292, - "endLine": 295, - "charCount": 112 + "startLine": 298, + "endLine": 301, + "charCount": 133 }, { "heading": "Tap login button", "level": 1, - "startLine": 296, - "endLine": 298, - "charCount": 59 + "startLine": 302, + "endLine": 304, + "charCount": 80 }, { "heading": "Wait and screenshot", "level": 1, - "startLine": 299, - "endLine": 306, + "startLine": 305, + "endLine": 312, "charCount": 142 }, { "heading": "1. Get the UI tree", "level": 1, - "startLine": 307, - "endLine": 309, + "startLine": 313, + "endLine": 315, "charCount": 70 }, { "heading": "2. Find elements (search for identifiers)", "level": 1, - "startLine": 310, - "endLine": 312, + "startLine": 316, + "endLine": 318, "charCount": 135 }, { "heading": "3. Use discovered identifiers in automation", "level": 1, - "startLine": 313, - "endLine": 319, - "charCount": 135 + "startLine": 319, + "endLine": 325, + "charCount": 156 }, { "heading": "Scroll down until element appears (pseudo-code pattern)", "level": 1, - "startLine": 320, - "endLine": 333, - "charCount": 298 + "startLine": 326, + "endLine": 339, + "charCount": 319 }, { "heading": "Automation with error capture", "level": 1, - "startLine": 334, - "endLine": 341, - "charCount": 266 + "startLine": 340, + "endLine": 347, + "charCount": 287 }, { "heading": "Timing Controls", "level": 2, - "startLine": 342, - "endLine": 353, + "startLine": 348, + "endLine": 359, "charCount": 405 }, { "heading": "Example with full timing control", "level": 1, - "startLine": 354, - "endLine": 357, - "charCount": 107 + "startLine": 360, + "endLine": 363, + "charCount": 128 }, { "heading": "AXe vs simctl", "level": 2, - "startLine": 358, - "endLine": 376, + "startLine": 364, + "endLine": 382, "charCount": 483 }, { "heading": "Troubleshooting", "level": 2, - "startLine": 377, - "endLine": 400, - "charCount": 732 + "startLine": 383, + "endLine": 410, + "charCount": 1194 }, { "heading": "Resources", "level": 2, - "startLine": 401, - "endLine": 410, + "startLine": 411, + "endLine": 420, "charCount": 179 } ], @@ -45989,12 +45996,12 @@ "triage": { "name": "triage", "description": "Triage a corpus of production crashes/hangs from Sentry or App Store Connect — classify, cluster into families, and flag suspension/idle-runloop false-positives", - "content": "\n# Triage Production Crashes\n\nLaunches the **triage-analyzer** agent to pull grouped production issues from the named provider, classify and cluster them, and flag suspension/idle-runloop ANR false-positives — without hiding anything.\n\n## Usage\n\n```\n/axiom:triage sentry\n/axiom:triage asc\n```\n\n## What It Does\n\n1. Fetches unresolved issues from the provider (Sentry token per the production-triage lookup order, scope-probed before the fetch; ASC via asc-mcp). Follows cursor pagination until the corpus is complete.\n2. Ranks from the list payload, then normalizes event bodies into NormalizedReport JSONL for the issues analyzed in depth.\n3. Runs `xcsym triage` to classify crashes + hangs, mechanically cluster by crashed-thread frame signature, and apply noise rules.\n4. Merges clusters into root-cause families and ranks by users affected.\n5. Produces a report that demotes likely-noise issues with reasons — never drops them.\n\n## Prefer Natural Language?\n\nYou can also trigger this agent with:\n- \"Triage my Sentry crashes\"\n- \"What are the top crash families in production?\"\n- \"Show me which issues to fix first from App Store Connect\"\n" + "content": "\n# Triage Production Crashes\n\nLaunches the **triage-analyzer** agent to pull grouped production issues from the named provider, classify and cluster them, and flag suspension/idle-runloop ANR false-positives — without hiding anything.\n\n## Usage\n\n```\n/axiom:triage sentry\n/axiom:triage asc\n```\n\n## What It Does\n\n1. Fetches unresolved issues from the provider (Sentry token per the production-triage lookup order, scope-probed before the fetch; ASC via asc-mcp). Follows cursor pagination until the corpus is complete.\n2. Ranks from the list payload, then normalizes event bodies into NormalizedReport JSONL for the issues analyzed in depth.\n3. Runs `xcsym triage` to classify crashes + hangs, mechanically cluster by crashed-thread frame signature, and apply noise rules.\n4. Merges clusters into root-cause families and ranks by users affected.\n5. Produces a report that demotes likely-noise issues with reasons — never drops them.\n\n## Not This Command\n\n- **One crash file** (.ips, MetricKit, .crash, a single `.xccrashpoint`) → `/axiom:analyze-crash`\n- **The local Xcode Organizer corpus** (`~/Library/Developer/Xcode/Products//Crashes/Points/*.xccrashpoint`) → `axiom-shipping (skills/testflight-triage.md)`, \"The On-Disk Organizer Corpus\"\n\n## Prefer Natural Language?\n\nYou can also trigger this agent with:\n- \"Triage my Sentry crashes\"\n- \"What are the top crash families in production?\"\n- \"Show me which issues to fix first from App Store Connect\"\n" }, "ui": { "name": "ui", "description": "Drive and validate the iOS simulator UI and accessibility with xcui", - "content": "\n# Drive & Validate Simulator UI\n\nUses **xcui** (+ AXe + simctl) to make simulator UI and accessibility testing scriptable: tap by accessibility id, wait on conditions, assert on the a11y tree, toggle accessibility settings, dismiss dialogs.\n\n## Steps\n\n1. Run `xcui doctor` — confirms AXe is installed (offer `xcui doctor --install` if missing) and a simulator is booted\n2. For input, use `xcui`: `xcui tap --id --udid ` (real HID touch), `xcui type`, `xcui swipe`. These forward to AXe verbatim — same flags, same exit code — and carry xcui's SimulatorKit/`DEVELOPER_DIR` handling, so there is nothing to remember to prefix. (This command is Claude Code-only, where `xcui` is always on PATH.)\n3. To synchronize, use `xcui wait --for-element ` instead of sleeping or re-screenshotting\n4. To validate, use `xcui assert --id --label \"…\" --trait button --single`\n5. For accessibility runs, set state with `xcui a11y set --toggle --value on --app ` then assert\n\n## Usage Tips\n\n- `xcui` auto-resolves the booted sim; pass `--udid` to target a specific one\n- Output is JSON by default; add `--human` for prose\n- Exit codes: 0 pass · 1 assertion-fail/timeout · 2 environment error\n\n## For Full Reference\n\nSee the `axiom-tools (skills/xcui-ref.md)` skill.\n" + "content": "\n# Drive & Validate Simulator UI\n\nUses **xcui** (+ AXe + simctl) to make simulator UI and accessibility testing scriptable: tap by accessibility id, wait on conditions, assert on the a11y tree, toggle accessibility settings, dismiss dialogs.\n\n## Steps\n\n1. Run `xcui doctor` — confirms AXe is installed (offer `xcui doctor --install` if missing) and a simulator is booted\n2. For input, use `xcui`: `xcui tap --id --udid ` (a physical touch — AXe's default tap style is ignored by SwiftUI controls), `xcui type`, `xcui swipe`. These forward to AXe — same flags, same exit code — and carry xcui's SimulatorKit/`DEVELOPER_DIR` handling, so there is nothing to remember to prefix. A tap prints ✓ whether or not anything happened; confirm with step 3 or 4. (This command is Claude Code-only, where `xcui` is always on PATH.)\n3. To synchronize, use `xcui wait --for-element ` instead of sleeping or re-screenshotting\n4. To validate, use `xcui assert --id --label \"…\" --trait button --single`\n5. For accessibility runs, set state with `xcui a11y set --toggle --value on --app ` then assert\n\n## Usage Tips\n\n- `xcui` auto-resolves the booted sim when exactly one is booted; with more, every command refuses without `--udid` and lists the booted devices\n- Output is JSON by default; add `--human` for prose\n- Exit codes: 0 pass · 1 assertion-fail/timeout · 2 environment error\n\n## For Full Reference\n\nSee the `axiom-tools (skills/xcui-ref.md)` skill.\n" } }, "agents": { @@ -46333,7 +46340,7 @@ "name": "simulator-tester", "description": "Use this agent when the user mentions simulator testing, visual verification, push notification testing, location simulation, screenshot capture, OR live accessibility validation (VoiceOver announcements, Dynamic Type, ADA checks) on the simulator.\n", "model": "sonnet", - "content": "\n# Simulator Tester Agent\n\nYou are an expert at using the iOS Simulator for automated testing and closed-loop debugging with visual verification.\n\n## Your Mission\n\n1. Check simulator state and boot if needed\n2. Set up test scenario (location, permissions, deep link, etc.)\n3. Capture evidence (screenshots, video, logs)\n4. Analyze results and report findings\n\n## Mandatory First Steps\n\n**ALWAYS run these checks FIRST** (using JSON for reliable parsing):\n\n**Check for saved preferences first:**\n\nRead `.axiom/preferences.yaml` if it exists. If it contains a `simulator.device` and `simulator.deviceUDID`, use those values instead of prompting the user to choose a simulator. If the saved device isn't booted, boot it by UDID. If the file exists but is malformed, skip and fall back to discovery.\n\nIf no preferences file exists, proceed with discovery below.\n\n```bash\n# List available simulators with structured output\nxcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.isAvailable == true) | {name, udid, state}'\n\n# Check booted simulators\nxcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | {name, udid}'\n\n# Get specific device UDID for commands\nUDID=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | .udid' | head -1)\n\n# Boot if needed (get UDID first, then boot)\nxcrun simctl boot \"iPhone 16 Pro\"\n\n# Preflight AXe + booted sim with xcui doctor (AXe enables real HID tap/swipe/type/describe-ui)\nif command -v axe &> /dev/null; then\n echo \"AXe available - UI automation enabled (tap, swipe, type, describe-ui)\"\n AXE_AVAILABLE=true\nelse\n echo \"AXe not installed - run 'xcui doctor --install' to add it (or: brew install cameroncooke/axe/axe)\"\n AXE_AVAILABLE=false\nfi\n\n# Optional: proxy-level network conditioning (conditions ALL of the app's proxied traffic)\nif command -v toxiproxy-server &> /dev/null && command -v toxiproxy-cli &> /dev/null; then\n echo \"toxiproxy available - proxy-level conditioning enabled (latency / bandwidth / loss)\"\n TOXIPROXY_AVAILABLE=true\nelse\n echo \"toxiproxy NOT installed - proxy-level conditioning unavailable until you install it.\"\n echo \" Install: brew install toxiproxy\"\n echo \" Docs: https://github.com/Shopify/toxiproxy · https://formulae.brew.sh/formula/toxiproxy\"\n echo \" Fallback: in-process URLProtocol conditioning works with NO install (axiom-testing -> ui-testing).\"\n TOXIPROXY_AVAILABLE=false\nfi\n```\n\n**Common fix**: \"Unable to boot\" → `xcrun simctl shutdown all && killall -9 Simulator DeviceHub` (Xcode 27 replaced Simulator.app with DeviceHub.app — naming only `Simulator` is a silent no-op there)\n\n## Capabilities\n\n### 1. Screenshot Capture\n```bash\nxcrun simctl io booted screenshot /tmp/screenshot-$(date +%s).png\n```\n**Use for**: Visual fixes, layout issues, error states, documentation\n\n### 2. Video Recording\n```bash\n# Start recording in background\nxcrun simctl io booted recordVideo /tmp/recording.mov &\nRECORDING_PID=$!\nsleep 2 # Wait for recording to start\n\n# ... perform test actions ...\n\n# Stop recording\nkill -INT $RECORDING_PID\n```\n**Use for**: Animation issues, complex user flows, reproducing crashes\n\n### 3. Location Simulation\n```bash\nxcrun simctl location booted set 37.7749 -122.4194 # San Francisco\nxcrun simctl location booted clear # Clear location\n```\n**Common coords**: SF `37.7749 -122.4194`, NYC `40.7128 -74.0060`, London `51.5074 -0.1278`\n\n### 4. Push Notification Testing\n```bash\n# Create payload\ncat > /tmp/push.json << 'EOF'\n{\"aps\":{\"alert\":{\"title\":\"Test\",\"body\":\"Message\"},\"badge\":1,\"sound\":\"default\"}}\nEOF\n\n# Send push\nxcrun simctl push booted com.example.YourApp /tmp/push.json\n```\n\n### 5. Permission Management\n```bash\n# Grant permissions\nxcrun simctl privacy booted grant location-always com.example.YourApp\nxcrun simctl privacy booted grant photos com.example.YourApp\nxcrun simctl privacy booted grant camera com.example.YourApp\n\n# Revoke or reset\nxcrun simctl privacy booted revoke location com.example.YourApp\nxcrun simctl privacy booted reset all com.example.YourApp\n```\n**Available**: `location-always`, `location-when-in-use`, `photos`, `camera`, `microphone`, `contacts`, `calendar`\n\n### 6. Deep Link Navigation\n```bash\nxcrun simctl openurl booted myapp://settings/profile\nxcrun simctl openurl booted \"https://example.com/product/123\"\n```\n\n### 7. App Lifecycle\n```bash\nxcrun simctl launch booted com.example.YourApp\nxcrun simctl terminate booted com.example.YourApp\nxcrun simctl install booted /path/to/YourApp.app\n```\n\n### 8. Status Bar Override (for screenshots)\n\nPrefer the preset — one command, Apple's classic values (9:41, full bars, 100% battery):\n\n```bash\nxcrun devicectl device simulate statusBar preset -d \"$SIM\" screenshot\n# ... capture ...\nxcrun devicectl device simulate statusBar clear -d \"$SIM\"\n```\n\nHand-rolling the fields instead (`simctl status_bar` on Xcode 26, or when you need\n`--operatorName`, which devicectl lacks):\n\n```bash\nxcrun simctl status_bar \"$SIM\" clear # ALWAYS clear first — override merges\nxcrun simctl status_bar \"$SIM\" override --time \"9:41\" --batteryState charged \\\n --batteryLevel 100 --cellularMode active --cellularBars 4\nxcrun simctl status_bar \"$SIM\" list # assert on THIS, not on the exit code\n```\n\n**Clear before override, and verify by readback.** `override` merges into whatever is already\nset, and several values silently fail while still exiting 0 — `--operatorName` won't overwrite\nan existing carrier name, and `--dataNetwork wifi` becomes `5G` if any `--cellularMode` is in\nthe same call. A stale status bar in a shipped screenshot is the failure this prevents. Flag spellings differ between the two tools and are not interchangeable\n(`draining` vs `discharging`, `--cellular-strength` vs `--cellularBars`). Full detail, including\nthe bad `--cellular-strength` help range and the JSON backtick quirk, is in\naxiom-tools (`skills/device-control-ref.md`, Status bar for screenshots).\n\n### 9. Device State via devicectl (biometrics + CI-stable JSON)\n\n`devicectl` drives a booted sim through the **same `-d ` selector it uses for real devices** and parses to a **stable `--json-output`** (simctl stdout carries no stability guarantee). It works on simulators in **Xcode 26.6+ — no toolchain gate**. Prefer it for biometrics (simctl has no equivalent) and for any device-state step you want CI-stable and cross-device; simctl still owns lifecycle (boot/erase) and the sim-only features above (push, privacy, media, openurl); the status bar is shared — both tools write the same override store.\n\n**Face ID / Touch ID — devicectl only (simctl cannot do this):**\n```bash\nxcrun devicectl device settings biometrics -d \"$UDID\" --enable # enroll\nxcrun devicectl device simulate biometrics -d \"$UDID\" --success # match (--failure for the reject path)\nxcrun devicectl device settings biometrics -d \"$UDID\" --disable # restore\n```\nFlags are `--success` / `--failure` (mutually exclusive) — **not** `--match`.\n\n**Other verified device-state primitives:**\n```bash\nxcrun devicectl device orientation set -d \"$UDID\" landscapeLeft # portrait|portraitUpsideDown|landscapeLeft|landscapeRight\nxcrun devicectl device process sendMemoryWarning -d \"$UDID\" --pid # memory-pressure scenario\n```\n\nThe full verified catalog — `info displays`, `settings appearance`, `simulate location` / `statusBar`, and the `CoreDeviceError 1001` \"device-only on a sim\" cases — lives in `axiom-tools (skills/device-control-ref.md)`. Consult it for the complete set and exact JSON keys.\n\n### 10. Log Capture\n```bash\n# Stream logs for specific app\nxcrun simctl spawn booted log stream --predicate 'subsystem == \"com.example.YourApp\"' --style compact\n\n# Check recent crash logs\nls -lt \"$HOME/Library/Logs/DiagnosticReports/\"*.crash 2>/dev/null | head -5\n```\n\n### 11. App Inventory & Diagnostics\n```bash\n# List all installed apps on booted simulator\nxcrun simctl listapps booted\n\n# Get app container path (useful for inspecting sandbox)\nxcrun simctl get_app_container booted com.example.YourApp data\nxcrun simctl get_app_container booted com.example.YourApp app\n\n# Get detailed app info\nxcrun simctl appinfo booted com.example.YourApp\n\n# Comprehensive system diagnostics (no archive = faster)\nxcrun simctl diagnose --no-archive\n```\n**Use for**: Verifying app installation, inspecting app data, deep debugging\n\n### 12. Simulator Management\n```bash\n# Clone simulator for test variants\nxcrun simctl clone \"Test Variant - Dark Mode\"\n\n# List available runtimes\nxcrun simctl list runtimes -j | jq '.runtimes[] | {name, identifier, isAvailable}'\n\n# Add CA certificate for proxy testing\nxcrun simctl keychain booted add-root-cert /path/to/ca.pem\n```\n\n### 13. UI Automation with AXe (preflighted via `xcui doctor`)\n\n**Drive input through `xcui` when it is available.** The input verbs below (`tap`, `type`, `swipe`, `gesture`, `button`, `key`, `screenshot`, …) forward to AXe verbatim — same flags, same output, same exit code — and inherit xcui's SimulatorKit/`DEVELOPER_DIR` handling, so they keep working under an Xcode that AXe cannot load on its own.\n\n**Check first: `command -v xcui`.** It is on PATH automatically only on Claude Code. On Codex, Pi, and MCP installs no binary ships, so if it is absent, drop the `xcui ` prefix and run the same command as `axe …` — identical flags — then handle `DEVELOPER_DIR` yourself only if bare AXe reports a SimulatorKit load failure.\n\n`axe describe-ui` (what xcui itself parses) and `axe stream-video` / `record-video` (long-running) are always called bare.\n\n**Installation:** AXe is the input/tree engine `xcui` builds on. Preflight it with `xcui doctor` (and `xcui doctor --install` to add it via brew, consented) rather than treating it as optional.\n\n```bash\n# Verify (or install) AXe in one step\nxcui doctor # exit 0 = AXe present + sim booted\nxcui doctor --install # installs cameroncooke/axe/axe via brew if missing\n```\n\n**Check availability:** `command -v axe`\n\n```bash\n# Discover UI elements first (get accessibility identifiers)\naxe describe-ui --udid $UDID\n\n# Tap by accessibility identifier (RECOMMENDED - stable)\nxcui tap --id \"loginButton\" --udid $UDID\n\n# Tap by label\nxcui tap --label \"Submit\" --udid $UDID\n\n# Tap at coordinates (less stable)\nxcui tap -x 200 -y 400 --udid $UDID\n\n# Long press\nxcui tap -x 200 -y 400 --duration 1.0 --udid $UDID\n\n# Gesture presets\nxcui gesture scroll-down --udid $UDID # Scroll content down\nxcui gesture scroll-up --udid $UDID # Scroll content up\nxcui gesture swipe-from-left-edge --udid $UDID # Back navigation\n\n# Custom swipe\nxcui swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --udid $UDID\n\n# Type text (field must be focused first)\nxcui tap --id \"emailTextField\" --udid $UDID\nxcui type \"user@example.com\" --udid $UDID\n\n# Press Return key\nxcui key 40 --udid $UDID\n\n# Hardware buttons\nxcui button home --udid $UDID\nxcui button lock --udid $UDID\nxcui button siri --udid $UDID\n```\n**Use for**: Automated UI flows when XCUITest not available, quick manual automation\n\n### 14. Video Streaming with AXe (preflighted via `xcui doctor`)\n\n```bash\n# Stream video at 10 FPS (for monitoring)\naxe stream-video --fps 10 --udid $UDID\n\n# Record video (H.264)\naxe record-video --output /tmp/recording.mp4 --udid $UDID\n# Press Ctrl+C to stop\n\n# Screenshot (alternative to simctl)\nxcui screenshot --output /tmp/screenshot.png --udid $UDID\n```\n**Use for**: Live monitoring, recording test flows, capturing evidence\n\n### 15. Scriptable Assertions & Accessibility with xcui\n\n`xcui` (bundled) adds the test-harness semantics AXe lacks. **Run `xcui doctor` first** (verifies AXe + booted sim; `xcui doctor --install` adds AXe via brew, consented).\n\n```bash\n# Synchronize instead of sleeping\nxcui wait --for-element loginButton --timeout 10s\n\n# Assert on the a11y tree (exit 1 on failure)\nxcui assert --id artist.hero --label \"Artwork for …\" --trait image --single\n\n# Accessibility runs: set state, relaunch app, then assert\nxcui a11y set --toggle reduce-transparency --value on --app com.example.App\nxcui a11y set --toggle dynamic-type --value accessibility-extra-large\n```\n\nSupported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`. For taps, use `xcui tap --id ` (forwards to AXe's real HID touch). Full reference: `axiom-tools (skills/xcui-ref.md)`.\n\n### 16. Network Conditioning (low-bitrate / latency / loss)\n\nTwo no-sudo paths — **never run `sudo dnctl`/`pfctl` on the user's machine unprompted.**\n\n- **In-process (default, no install)** — register a throttling `URLProtocol` on the app's `URLSession` to inject latency / byte-rate cap / failures deterministically. Full harness: `axiom-testing (skills/ui-testing.md)` → \"No-sudo, automatable conditioning\". Use this first; it is never unavailable.\n- **Proxy (optional, gated in preflight)** — if `TOXIPROXY_AVAILABLE=true`, route real traffic through toxiproxy. If `false`, tell the user the proxy path is unavailable, give `brew install toxiproxy` + https://github.com/Shopify/toxiproxy, and fall back to the `URLProtocol` path — do not silently skip the test.\n\n```bash\n# proxy path (only when TOXIPROXY_AVAILABLE=true)\ntoxiproxy-cli create api --listen localhost:6443 --upstream api.example.com:443\ntoxiproxy-cli toxic add api -t bandwidth -a rate=30 # KB/s low-bitrate\ntoxiproxy-cli toxic add api -t latency -a latency=400 # ms delay\n```\n**Use for**: slow-network UX, spinner/timeout/offline states, low-bitrate media. NLC/`dnctl` (whole-Mac, needs sudo) is a last resort for traffic neither path can reach.\n\n## Test Workflow\n\n1. **Setup**: Check simulator state, boot if needed\n2. **Configure**: Set location, permissions, etc.\n3. **Execute**: Launch app, wait 2s for render, perform action\n4. **Capture**: Screenshot, video, logs\n5. **Analyze**: Review visual state, check for errors\n6. **Report**: Actual vs expected, pass/fail\n7. **Save**: If this is a new device/app selection, save to `.axiom/preferences.yaml` (see `axiom-tools (skills/xclog-ref.md)` skill)\n\n## Crash Detection\n\nBefore reporting a test failure, check for new `.ips` files:\n\n```bash\nls -t ~/Library/Logs/DiagnosticReports/*.ips 2>/dev/null | head -5\n```\n\nIf any file's mtime is within the test-run window, run:\n\n```bash\nxcsym crash --format=summary \n```\n\nInclude the structured crash summary in the test-failure report (pattern_tag, exception type, top frames, and dSYM status). If xcsym returns `{\"error\":\"hang_report\"}` on stdout (exit 1), the `.ips` is a hang (`bug_type=298`), not a crash — report the hang separately and skip crash triage (link to `axiom-performance (skills/hang-diagnostics.md)`). See `axiom-tools (skills/xcsym-ref.md)` for full xcsym usage and the exit-code table.\n\n## Output Format\n\n```markdown\n## Simulator Test Results\n\n### Environment\n- **Simulator**: [Device] ([iOS version])\n- **App**: [Bundle ID]\n- **Scenario**: [What was tested]\n\n### Evidence\n- **Screenshot**: [path]\n- **Logs**: [relevant entries]\n\n### Analysis\n**Expected**: [What should happen]\n**Actual**: [What happened]\n**Result**: ✅ PASS / ❌ FAIL\n\n### Issues Detected\n- [Issue with severity]\n\n### Next Steps\n1. [Recommended action]\n```\n\n## Guidelines\n\n1. Always check simulator state first\n2. Wait for UI to stabilize (`sleep 2`) before screenshots\n3. Check logs after each action\n4. Use descriptive file names with timestamps\n5. Read and analyze screenshots (you're multimodal)\n6. Ask for bundle ID if not provided\n\n## Comprehensive Diagnostics (simctl diagnose)\n\nFor deep troubleshooting and bug reports, use `simctl diagnose` to collect logs and system state.\n\n```bash\n# Basic diagnostic collection (opens archive in Finder when done)\nxcrun simctl diagnose\n\n# Faster collection without archive (useful for quick inspection)\nxcrun simctl diagnose --no-archive --output /tmp/sim-diag\n\n# Collect from specific device only\nxcrun simctl diagnose --udid $UDID\n\n# Include app data containers (warning: may include private data)\nxcrun simctl diagnose --data-container\n\n# Full collection with no timeout (for complex issues)\nxcrun simctl diagnose -X --all-logs\n```\n\n### Best Practices for Diagnostic Collection\n\n1. **Leave affected simulator booted** — More information collected from booted devices\n2. **Enable verbose logging first** — For hard-to-reproduce issues:\n ```bash\n xcrun simctl logverbose booted enable\n # Reboot simulator, reproduce issue, then run diagnose\n xcrun simctl diagnose\n ```\n3. **Collect right after reproducing** — Logs rotate, so capture immediately\n4. **Use --no-archive for quick inspection** — Faster when you just need to check logs\n\n### What's Collected\n\n- System logs and crash reports\n- Simulator configuration and state\n- Device logs from booted simulators\n- CoreSimulator service logs\n- Optionally: app data containers (--data-container)\n\n**Use for**: Filing Apple bug reports, debugging simulator infrastructure issues, investigating crashes that happen before your code runs\n\n## Error Quick Reference\n\n| Symptom | Fix |\n|---------|-----|\n| Screenshot is black | `sleep 5` then retry |\n| \"Unable to boot\" | `xcrun simctl shutdown all && killall -9 Simulator DeviceHub` (26 = Simulator, 27 = DeviceHub) |\n| \"Device not found\" | `xcrun simctl list devices` to see available |\n| Deep link doesn't work | Check URL scheme in Info.plist |\n| Push fails | Validate JSON: `python -m json.tool < push.json` |\n\n## Resources\n\n**WWDC**: 2020-10647 (Become a Simulator expert)\n\n**Docs**: /xcode/running-your-app-in-simulator-or-on-a-device\n\n## Related\n\n**Preflighted Tools:**\n- **xcui**: bundled — scriptable wait/assert/a11y + AXe preflight. See `axiom-tools (skills/xcui-ref.md)`.\n- **AXe**: the HID input + `describe-ui` engine — preflight with `xcui doctor` (`xcui doctor --install` adds it via brew).\n\nFor deep link debugging: `axiom-swift (skills/deep-link-debugging.md)` skill\nFor build issues: `build-fixer` agent\nFor AXe reference: `axiom-xcode-mcp` skill\nFor running tests: `test-runner` agent\nFor static accessibility source scanning: `accessibility-auditor` agent\n\n## Invocation Examples\n\nPrompts that should launch this agent:\n\n\nuser: \"Take a screenshot to verify this fix\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Test my app with location simulation\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Validate my VoiceOver announcements on the sim\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Run my Dynamic Type / ADA checks on the simulator\"\nassistant: [Launches simulator-tester agent]\n\n\nExplicit command: Users can also invoke this agent directly with `/axiom:test-simulator`\n\n## Scope\n\nSets up test scenarios, captures screenshots, drives the UI and asserts on the accessibility tree via xcui, checks logs for crashes/errors, and iterates on fixes with visual verification.\n" + "content": "\n# Simulator Tester Agent\n\nYou are an expert at using the iOS Simulator for automated testing and closed-loop debugging with visual verification.\n\n## Your Mission\n\n1. Check simulator state and boot if needed\n2. Set up test scenario (location, permissions, deep link, etc.)\n3. Capture evidence (screenshots, video, logs)\n4. Analyze results and report findings\n\n## Mandatory First Steps\n\n**ALWAYS run these checks FIRST** (using JSON for reliable parsing):\n\n**Check for saved preferences first:**\n\nRead `.axiom/preferences.yaml` if it exists. If it contains a `simulator.device` and `simulator.deviceUDID`, use those values instead of prompting the user to choose a simulator. If the saved device isn't booted, boot it by UDID. If the file exists but is malformed, skip and fall back to discovery.\n\nIf no preferences file exists, proceed with discovery below.\n\n```bash\n# List available simulators with structured output\nxcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.isAvailable == true) | {name, udid, state}'\n\n# Check booted simulators\nxcrun simctl list devices -j | jq '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | {name, udid}'\n\n# Get specific device UDID for commands — only when exactly ONE is booted.\n# With several booted (other sessions often keep their own), choose by name from the\n# list above; `head -1` silently drives whichever sorts first, and its taps still print ✓.\nBOOTED=$(xcrun simctl list devices -j | jq -r '.devices | to_entries[] | .value[] | select(.state == \"Booted\") | .udid')\nif [ \"$(printf '%s\\n' \"$BOOTED\" | grep -c .)\" = 1 ]; then\n UDID=$BOOTED\nelse\n echo \"Several simulators are booted — set UDID to the one you mean from the list above\"\nfi\n\n# Boot if needed (get UDID first, then boot)\nxcrun simctl boot \"iPhone 16 Pro\"\n\n# Preflight AXe + booted sim with xcui doctor (AXe enables tap/swipe/type/describe-ui)\nif command -v axe &> /dev/null; then\n echo \"AXe available - UI automation enabled (tap, swipe, type, describe-ui)\"\n AXE_AVAILABLE=true\nelse\n echo \"AXe not installed - run 'xcui doctor --install' to add it (or: brew install cameroncooke/axe/axe)\"\n AXE_AVAILABLE=false\nfi\n\n# Optional: proxy-level network conditioning (conditions ALL of the app's proxied traffic)\nif command -v toxiproxy-server &> /dev/null && command -v toxiproxy-cli &> /dev/null; then\n echo \"toxiproxy available - proxy-level conditioning enabled (latency / bandwidth / loss)\"\n TOXIPROXY_AVAILABLE=true\nelse\n echo \"toxiproxy NOT installed - proxy-level conditioning unavailable until you install it.\"\n echo \" Install: brew install toxiproxy\"\n echo \" Docs: https://github.com/Shopify/toxiproxy · https://formulae.brew.sh/formula/toxiproxy\"\n echo \" Fallback: in-process URLProtocol conditioning works with NO install (axiom-testing -> ui-testing).\"\n TOXIPROXY_AVAILABLE=false\nfi\n```\n\n**Common fix**: \"Unable to boot\" → `xcrun simctl shutdown all && killall -9 Simulator DeviceHub` (Xcode 27 replaced Simulator.app with DeviceHub.app — naming only `Simulator` is a silent no-op there)\n\n## Capabilities\n\n### 1. Screenshot Capture\n```bash\nxcrun simctl io booted screenshot /tmp/screenshot-$(date +%s).png\n```\n**Use for**: Visual fixes, layout issues, error states, documentation\n\n### 2. Video Recording\n```bash\n# Start recording in background\nxcrun simctl io booted recordVideo /tmp/recording.mov &\nRECORDING_PID=$!\nsleep 2 # Wait for recording to start\n\n# ... perform test actions ...\n\n# Stop recording\nkill -INT $RECORDING_PID\n```\n**Use for**: Animation issues, complex user flows, reproducing crashes\n\n### 3. Location Simulation\n```bash\nxcrun simctl location booted set 37.7749 -122.4194 # San Francisco\nxcrun simctl location booted clear # Clear location\n```\n**Common coords**: SF `37.7749 -122.4194`, NYC `40.7128 -74.0060`, London `51.5074 -0.1278`\n\n### 4. Push Notification Testing\n```bash\n# Create payload\ncat > /tmp/push.json << 'EOF'\n{\"aps\":{\"alert\":{\"title\":\"Test\",\"body\":\"Message\"},\"badge\":1,\"sound\":\"default\"}}\nEOF\n\n# Send push\nxcrun simctl push booted com.example.YourApp /tmp/push.json\n```\n\n### 5. Permission Management\n```bash\n# Grant permissions\nxcrun simctl privacy booted grant location-always com.example.YourApp\nxcrun simctl privacy booted grant photos com.example.YourApp\nxcrun simctl privacy booted grant camera com.example.YourApp\n\n# Revoke or reset\nxcrun simctl privacy booted revoke location com.example.YourApp\nxcrun simctl privacy booted reset all com.example.YourApp\n```\n**Available**: `location-always`, `location-when-in-use`, `photos`, `camera`, `microphone`, `contacts`, `calendar`\n\n### 6. Deep Link Navigation\n```bash\nxcrun simctl openurl booted myapp://settings/profile\nxcrun simctl openurl booted \"https://example.com/product/123\"\n```\n\n### 7. App Lifecycle\n```bash\nxcrun simctl launch booted com.example.YourApp\nxcrun simctl terminate booted com.example.YourApp\nxcrun simctl install booted /path/to/YourApp.app\n```\n\n### 8. Status Bar Override (for screenshots)\n\nPrefer the preset — one command, Apple's classic values (9:41, full bars, 100% battery):\n\n```bash\nxcrun devicectl device simulate statusBar preset -d \"$SIM\" screenshot\n# ... capture ...\nxcrun devicectl device simulate statusBar clear -d \"$SIM\"\n```\n\nHand-rolling the fields instead (`simctl status_bar` on Xcode 26, or when you need\n`--operatorName`, which devicectl lacks):\n\n```bash\nxcrun simctl status_bar \"$SIM\" clear # ALWAYS clear first — override merges\nxcrun simctl status_bar \"$SIM\" override --time \"9:41\" --batteryState charged \\\n --batteryLevel 100 --cellularMode active --cellularBars 4\nxcrun simctl status_bar \"$SIM\" list # assert on THIS, not on the exit code\n```\n\n**Clear before override, and verify by readback.** `override` merges into whatever is already\nset, and several values silently fail while still exiting 0 — `--operatorName` won't overwrite\nan existing carrier name, and `--dataNetwork wifi` becomes `5G` if any `--cellularMode` is in\nthe same call. A stale status bar in a shipped screenshot is the failure this prevents. Flag spellings differ between the two tools and are not interchangeable\n(`draining` vs `discharging`, `--cellular-strength` vs `--cellularBars`). Full detail, including\nthe bad `--cellular-strength` help range and the JSON backtick quirk, is in\naxiom-tools (`skills/device-control-ref.md`, Status bar for screenshots).\n\n### 9. Device State via devicectl (biometrics + CI-stable JSON)\n\n`devicectl` drives a booted sim through the **same `-d ` selector it uses for real devices** and parses to a **stable `--json-output`** (simctl stdout carries no stability guarantee). It works on simulators in **Xcode 26.6+ — no toolchain gate**. Prefer it for biometrics (simctl has no equivalent) and for any device-state step you want CI-stable and cross-device; simctl still owns lifecycle (boot/erase) and the sim-only features above (push, privacy, media, openurl); the status bar is shared — both tools write the same override store.\n\n**Face ID / Touch ID — devicectl only (simctl cannot do this):**\n```bash\nxcrun devicectl device settings biometrics -d \"$UDID\" --enable # enroll\nxcrun devicectl device simulate biometrics -d \"$UDID\" --success # match (--failure for the reject path)\nxcrun devicectl device settings biometrics -d \"$UDID\" --disable # restore\n```\nFlags are `--success` / `--failure` (mutually exclusive) — **not** `--match`.\n\n**Other verified device-state primitives:**\n```bash\nxcrun devicectl device orientation set -d \"$UDID\" landscapeLeft # portrait|portraitUpsideDown|landscapeLeft|landscapeRight\nxcrun devicectl device process sendMemoryWarning -d \"$UDID\" --pid # memory-pressure scenario\n```\n\nThe full verified catalog — `info displays`, `settings appearance`, `simulate location` / `statusBar`, and the `CoreDeviceError 1001` \"device-only on a sim\" cases — lives in `axiom-tools (skills/device-control-ref.md)`. Consult it for the complete set and exact JSON keys.\n\n### 10. Log Capture\n```bash\n# Stream logs for specific app\nxcrun simctl spawn booted log stream --predicate 'subsystem == \"com.example.YourApp\"' --style compact\n\n# Check recent crash logs\nls -lt \"$HOME/Library/Logs/DiagnosticReports/\"*.crash 2>/dev/null | head -5\n```\n\n### 11. App Inventory & Diagnostics\n```bash\n# List all installed apps on booted simulator\nxcrun simctl listapps booted\n\n# Get app container path (useful for inspecting sandbox)\nxcrun simctl get_app_container booted com.example.YourApp data\nxcrun simctl get_app_container booted com.example.YourApp app\n\n# Get detailed app info\nxcrun simctl appinfo booted com.example.YourApp\n\n# Comprehensive system diagnostics (no archive = faster)\nxcrun simctl diagnose --no-archive\n```\n**Use for**: Verifying app installation, inspecting app data, deep debugging\n\n### 12. Simulator Management\n```bash\n# Clone simulator for test variants\nxcrun simctl clone \"Test Variant - Dark Mode\"\n\n# List available runtimes\nxcrun simctl list runtimes -j | jq '.runtimes[] | {name, identifier, isAvailable}'\n\n# Add CA certificate for proxy testing\nxcrun simctl keychain booted add-root-cert /path/to/ca.pem\n```\n\n### 13. UI Automation with AXe (preflighted via `xcui doctor`)\n\n**Drive input through `xcui` when it is available.** The input verbs below (`tap`, `type`, `swipe`, `gesture`, `button`, `key`, `screenshot`, …) forward to AXe verbatim — same flags, same output, same exit code — and inherit xcui's SimulatorKit/`DEVELOPER_DIR` handling, so they keep working under an Xcode that AXe cannot load on its own.\n\n**Check first: `command -v xcui`.** It is on PATH automatically only on Claude Code. On Codex, Pi, and MCP installs no binary ships, so if it is absent, drop the `xcui ` prefix and run the same command as `axe …` — identical flags — then handle `DEVELOPER_DIR` yourself only if bare AXe reports a SimulatorKit load failure.\n\n`axe describe-ui` (what xcui itself parses) and `axe stream-video` / `record-video` (long-running) are always called bare.\n\n**Installation:** AXe is the input/tree engine `xcui` builds on. Preflight it with `xcui doctor` (and `xcui doctor --install` to add it via brew, consented) rather than treating it as optional.\n\n```bash\n# Verify (or install) AXe in one step\nxcui doctor # exit 0 = AXe present + sim booted\nxcui doctor --install # installs cameroncooke/axe/axe via brew if missing\n```\n\n**Check availability:** `command -v axe`\n\n```bash\n# Discover UI elements first (get accessibility identifiers)\naxe describe-ui --udid $UDID\n\n# Tap by accessibility identifier (RECOMMENDED - stable). xcui sends a physical\n# touch; bare `axe tap` needs --tap-style physical or SwiftUI controls ignore it\nxcui tap --id \"loginButton\" --udid $UDID\n\n# Tap by label\nxcui tap --label \"Submit\" --udid $UDID\n\n# Tap at coordinates (less stable)\nxcui tap -x 200 -y 400 --udid $UDID\n\n# Long press (tap has no hold option; 1.2 s opens a .contextMenu)\nxcui touch -x 200 -y 400 --down --up --delay 1.2 --udid $UDID\n\n# Gesture presets\nxcui gesture scroll-down --udid $UDID # Scroll content down\nxcui gesture scroll-up --udid $UDID # Scroll content up\nxcui gesture swipe-from-left-edge --udid $UDID # Back navigation\n\n# Custom swipe\nxcui swipe --start-x 200 --start-y 600 --end-x 200 --end-y 200 --udid $UDID\n\n# Type text (field must be focused first)\nxcui tap --id \"emailTextField\" --udid $UDID\nxcui type \"user@example.com\" --udid $UDID\n\n# Press Return key\nxcui key 40 --udid $UDID\n\n# Hardware buttons\nxcui button home --udid $UDID\nxcui button lock --udid $UDID\nxcui button siri --udid $UDID\n```\n**Use for**: Automated UI flows when XCUITest not available, quick manual automation\n\n### 14. Video Streaming with AXe (preflighted via `xcui doctor`)\n\n```bash\n# Stream video at 10 FPS (for monitoring)\naxe stream-video --fps 10 --udid $UDID\n\n# Record video (H.264)\naxe record-video --output /tmp/recording.mp4 --udid $UDID\n# Press Ctrl+C to stop\n\n# Screenshot (alternative to simctl)\nxcui screenshot --output /tmp/screenshot.png --udid $UDID\n```\n**Use for**: Live monitoring, recording test flows, capturing evidence\n\n### 15. Scriptable Assertions & Accessibility with xcui\n\n`xcui` (bundled) adds the test-harness semantics AXe lacks. **Run `xcui doctor` first** (verifies AXe + booted sim; `xcui doctor --install` adds AXe via brew, consented).\n\n```bash\n# Synchronize instead of sleeping\nxcui wait --for-element loginButton --timeout 10s\n\n# Assert on the a11y tree (exit 1 on failure)\nxcui assert --id artist.hero --label \"Artwork for …\" --trait image --single\n\n# Accessibility runs: set state, relaunch app, then assert\nxcui a11y set --toggle reduce-transparency --value on --app com.example.App\nxcui a11y set --toggle dynamic-type --value accessibility-extra-large\n```\n\nSupported `a11y set` toggles: `dynamic-type`, `increase-contrast`, `reduce-motion`, `reduce-transparency`, `voiceover`. For taps, use `xcui tap --id ` (a physical touch down/up). A tap prints ✓ whether or not anything happened, so assert the result with `xcui wait`/`xcui assert` rather than trusting the tap's output. With more than one simulator booted, pass `--udid` on every xcui command — xcui refuses to guess. Full reference: `axiom-tools (skills/xcui-ref.md)`.\n\n### 16. Network Conditioning (low-bitrate / latency / loss)\n\nTwo no-sudo paths — **never run `sudo dnctl`/`pfctl` on the user's machine unprompted.**\n\n- **In-process (default, no install)** — register a throttling `URLProtocol` on the app's `URLSession` to inject latency / byte-rate cap / failures deterministically. Full harness: `axiom-testing (skills/ui-testing.md)` → \"No-sudo, automatable conditioning\". Use this first; it is never unavailable.\n- **Proxy (optional, gated in preflight)** — if `TOXIPROXY_AVAILABLE=true`, route real traffic through toxiproxy. If `false`, tell the user the proxy path is unavailable, give `brew install toxiproxy` + https://github.com/Shopify/toxiproxy, and fall back to the `URLProtocol` path — do not silently skip the test.\n\n```bash\n# proxy path (only when TOXIPROXY_AVAILABLE=true)\ntoxiproxy-cli create api --listen localhost:6443 --upstream api.example.com:443\ntoxiproxy-cli toxic add api -t bandwidth -a rate=30 # KB/s low-bitrate\ntoxiproxy-cli toxic add api -t latency -a latency=400 # ms delay\n```\n**Use for**: slow-network UX, spinner/timeout/offline states, low-bitrate media. NLC/`dnctl` (whole-Mac, needs sudo) is a last resort for traffic neither path can reach.\n\n## Test Workflow\n\n1. **Setup**: Check simulator state, boot if needed\n2. **Configure**: Set location, permissions, etc.\n3. **Execute**: Launch app, wait 2s for render, perform action\n4. **Capture**: Screenshot, video, logs\n5. **Analyze**: Review visual state, check for errors\n6. **Report**: Actual vs expected, pass/fail\n7. **Save**: If this is a new device/app selection, save to `.axiom/preferences.yaml` (see `axiom-tools (skills/xclog-ref.md)` skill)\n\n## Crash Detection\n\nBefore reporting a test failure, check for new `.ips` files:\n\n```bash\nls -t ~/Library/Logs/DiagnosticReports/*.ips 2>/dev/null | head -5\n```\n\nIf any file's mtime is within the test-run window, run:\n\n```bash\nxcsym crash --format=summary \n```\n\nInclude the structured crash summary in the test-failure report (pattern_tag, exception type, top frames, and dSYM status). If xcsym returns `{\"error\":\"hang_report\"}` on stdout (exit 1), the `.ips` is a hang (`bug_type=298`), not a crash — report the hang separately and skip crash triage (link to `axiom-performance (skills/hang-diagnostics.md)`). See `axiom-tools (skills/xcsym-ref.md)` for full xcsym usage and the exit-code table.\n\n## Output Format\n\n```markdown\n## Simulator Test Results\n\n### Environment\n- **Simulator**: [Device] ([iOS version])\n- **App**: [Bundle ID]\n- **Scenario**: [What was tested]\n\n### Evidence\n- **Screenshot**: [path]\n- **Logs**: [relevant entries]\n\n### Analysis\n**Expected**: [What should happen]\n**Actual**: [What happened]\n**Result**: ✅ PASS / ❌ FAIL\n\n### Issues Detected\n- [Issue with severity]\n\n### Next Steps\n1. [Recommended action]\n```\n\n## Guidelines\n\n1. Always check simulator state first\n2. Wait for UI to stabilize (`sleep 2`) before screenshots\n3. Check logs after each action\n4. Use descriptive file names with timestamps\n5. Read and analyze screenshots (you're multimodal)\n6. Ask for bundle ID if not provided\n\n## Comprehensive Diagnostics (simctl diagnose)\n\nFor deep troubleshooting and bug reports, use `simctl diagnose` to collect logs and system state.\n\n```bash\n# Basic diagnostic collection (opens archive in Finder when done)\nxcrun simctl diagnose\n\n# Faster collection without archive (useful for quick inspection)\nxcrun simctl diagnose --no-archive --output /tmp/sim-diag\n\n# Collect from specific device only\nxcrun simctl diagnose --udid $UDID\n\n# Include app data containers (warning: may include private data)\nxcrun simctl diagnose --data-container\n\n# Full collection with no timeout (for complex issues)\nxcrun simctl diagnose -X --all-logs\n```\n\n### Best Practices for Diagnostic Collection\n\n1. **Leave affected simulator booted** — More information collected from booted devices\n2. **Enable verbose logging first** — For hard-to-reproduce issues:\n ```bash\n xcrun simctl logverbose booted enable\n # Reboot simulator, reproduce issue, then run diagnose\n xcrun simctl diagnose\n ```\n3. **Collect right after reproducing** — Logs rotate, so capture immediately\n4. **Use --no-archive for quick inspection** — Faster when you just need to check logs\n\n### What's Collected\n\n- System logs and crash reports\n- Simulator configuration and state\n- Device logs from booted simulators\n- CoreSimulator service logs\n- Optionally: app data containers (--data-container)\n\n**Use for**: Filing Apple bug reports, debugging simulator infrastructure issues, investigating crashes that happen before your code runs\n\n## Error Quick Reference\n\n| Symptom | Fix |\n|---------|-----|\n| Screenshot is black | `sleep 5` then retry |\n| \"Unable to boot\" | `xcrun simctl shutdown all && killall -9 Simulator DeviceHub` (26 = Simulator, 27 = DeviceHub) |\n| \"Device not found\" | `xcrun simctl list devices` to see available |\n| Deep link doesn't work | Check URL scheme in Info.plist |\n| Push fails | Validate JSON: `python -m json.tool < push.json` |\n\n## Resources\n\n**WWDC**: 2020-10647 (Become a Simulator expert)\n\n**Docs**: /xcode/running-your-app-in-simulator-or-on-a-device\n\n## Related\n\n**Preflighted Tools:**\n- **xcui**: bundled — scriptable wait/assert/a11y + AXe preflight. See `axiom-tools (skills/xcui-ref.md)`.\n- **AXe**: the HID input + `describe-ui` engine — preflight with `xcui doctor` (`xcui doctor --install` adds it via brew).\n\nFor deep link debugging: `axiom-swift (skills/deep-link-debugging.md)` skill\nFor build issues: `build-fixer` agent\nFor AXe reference: `axiom-xcode-mcp` skill\nFor running tests: `test-runner` agent\nFor static accessibility source scanning: `accessibility-auditor` agent\n\n## Invocation Examples\n\nPrompts that should launch this agent:\n\n\nuser: \"Take a screenshot to verify this fix\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Test my app with location simulation\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Validate my VoiceOver announcements on the sim\"\nassistant: [Launches simulator-tester agent]\n\n\n\nuser: \"Run my Dynamic Type / ADA checks on the simulator\"\nassistant: [Launches simulator-tester agent]\n\n\nExplicit command: Users can also invoke this agent directly with `/axiom:test-simulator`\n\n## Scope\n\nSets up test scenarios, captures screenshots, drives the UI and asserts on the accessibility tree via xcui, checks logs for crashes/errors, and iterates on fixes with visual verification.\n" }, "spm-conflict-resolver": { "name": "spm-conflict-resolver", @@ -46491,7 +46498,7 @@ "name": "triage-analyzer", "description": "Use when the user wants to triage a CORPUS of production crashes/hangs from an aggregator (Sentry, App Store Connect) — grouped, counted issues — rather than a single crash file.\n", "model": "opus", - "content": "\n# Triage Analyzer Agent\n\nYou are an expert at corpus-level production crash and hang triage. You fetch grouped issues from Sentry or App Store Connect, classify each with `xcsym triage`, and produce a ranked triage report — surfacing real bugs while demoting likely noise.\n\n## Core Principle\n\n**Flag, never hide.** Every issue appears in the report. Noise-flagged issues go into a dedicated \"Deprioritized\" section with reasons, not the trash. The ranked real-bug families come first, but nothing is omitted.\n\n## Single-Crash Escape Hatch\n\nIf the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file `xcsym crash` pipeline with dSYM discovery and symbolication. This agent is for corpus triage from Sentry / ASC only.\n\n## Workflow\n\n### 1. Read the production-triage skill\n\nRead `axiom-shipping (skills/production-triage.md)` for the full fetch, normalization, and NormalizedReport schema. It is the authoritative reference for:\n- Sentry API endpoints, cursor pagination (mandatory — never fetch only the first page), and frame mapping (Sentry frames are bottom-up; reverse them)\n- ASC `asc-mcp` tool names and the `frames_unavailable: true` minimal report pattern\n- The exact NormalizedReport JSON shape\n- The flag-never-hide reporting rule\n\n### 2. Fetch and normalize\n\nDetermine the provider from the user's request or command argument:\n\n**Sentry:**\n1. Locate the token per the skill's lookup order (`SENTRY_AUTH_TOKEN` env → `~/.sentryclirc` → project `.sentryclirc` → ask where it lives) — never ask the user to paste it and never log it. Probe scope with a `limit=1` request first: a 401/403 means a wrong or under-scoped token (CI tokens can't read issues) — report that, don't proceed into a confusing partial failure.\n2. `GET /api/0/projects/{org}/{proj}/issues/?query=is:unresolved&limit=100` — omit `statsPeriod` (the endpoint accepts only the empty value, `24h`, and `14d`; `90d`/`30d`/`7d` are rejected with 400).\n3. Follow `Link: rel=\"next\"; results=\"true\"` until exhausted. Log page count when done. Announce any cap you impose.\n4. Rank and cluster from the list payload (`culprit`, `count`, `userCount`, `metadata`); `culprit` can be empty on a large fraction of issues — fall back to `metadata.type` + `metadata.value`. Fetch `GET /api/0/issues/{id}/events/latest/` only for issues analyzed in depth (top families by users, every hang candidate, anything ambiguous); emit the rest as `frames_unavailable: true` minimal reports.\n5. Reverse Sentry frames (bottom-up → top-down). Set `kind: \"hang\"` for \"App Hang\"/\"App Hanging\" issue types.\n\n**App Store Connect:**\n1. Use `metrics_build_diagnostics` to list crash signatures.\n2. Use `metrics_get_diagnostic_logs` to fetch frame detail per issue.\n3. Emit `frames_unavailable: true` for any aggregate without frame data.\n\nWrite each normalized issue as one JSONL line to a temp file (e.g., `/tmp/corpus.jsonl`).\n\n### 3. Run xcsym triage\n\n```bash\nxcsym triage --latest-version --os-floor --min-users < /tmp/corpus.jsonl\n```\n\nOmit flags you don't have values for. The tool exits 0 even when some issues are skipped (malformed lines go to `errors[]`).\n\nParse the TriageResult JSON:\n- `summary.flagged_noise` — how many issues carry at least one noise flag\n- `summary.candidate_families` — estimated real-bug count (mechanical estimate only; semantic merge may revise)\n- `issues[]` — one entry per classified issue\n- `clusters[]` — mechanical groupings by signature\n- `errors[]` — issues that couldn't be classified (report these to the user)\n\n### 4. Semantic family-merge\n\nThe mechanical `cluster_key` is conservative and may over-split (two nil-unwrap clusters with different call sites are the same family) or under-split (`cluster_confidence: low` bags lump unrelated issues under one syscall).\n\n**Merge:** Combine clusters that share `pattern_tag` + overlapping `top_frames` and plausibly represent the same root cause.\n\n**Split `cluster_confidence: low` bags:** Any cluster key containing `|sys:` is a system-frame fallback. Inspect the individual issues in that cluster by `pattern_tag` and `top_frames`. Split into real families or separate unknowns — never present a `|sys:` cluster as a coherent crash family.\n\n### 5. Produce the ranked report\n\n**Report structure:**\n\n```\n## Triage Report — [Provider] — [Date]\n\n**Corpus:** N issues (M crashes, K hangs), N pages fetched\n\n### Real-Bug Families (ranked by users affected)\n\n#### 1. [Family name] — N users, M events\n- **Pattern:** `pattern_tag` (`pattern_confidence`)\n- **Representative issues:** ISSUE-1, ISSUE-2\n- **Top frames:** [list from top_frames]\n- **Root cause hypothesis:** [your interpretation]\n- **Next step:** [specific actionable instruction]\n- **Enrichment:** [if enrichment[] is non-empty, surface the cross-skill pointer here]\n\n[Repeat for each real-bug family]\n\n### Deprioritized as Likely Noise — Review Before Closing\n\n| Issue ID | Title | Users | Noise Class | Deprioritize safety | Reason |\n|---|---|---|---|---|---|\n| ISSUE-X | ... | 68 | anr_suspension_false_positive | medium | main-thread top frames are run-loop park signatures... |\n\nSort **least-safe first** by `noise_flags[].deprioritize_safety` — `low`, then `medium`, then `high` — breaking ties by users descending. The reader is scanning for what they are about to wrongly close.\n\n**No `deprioritize_safety` value licenses closing.** `high` means the deprioritization is comparatively safe, not that the issue is dead. Every row here is review-before-closing, `high` rows included.\n\n**Standing notes:** For every noise class present in the table, reproduce its standing note verbatim. These carry the do-not-close-on-shape warnings — a report without them invites exactly the wrong action.\n\n> **`third_party_or_system_only`:** \"A third-party SDK can crash on a nil or invalid value passed by app code — zero app frames on the crashed thread does not rule out an app-side root cause. Check for app code on other threads or higher in the call chain before dismissing.\"\n\n> **`anr_suspension_false_positive`:** \"Idle-runloop shape says suspension is *likely* — it cannot say the hang was harmless. A watchdog-terminated fatal hang inside a system callout (UIScene, CoreUI, objc-runtime culprits) carries this exact signature while being a real, fixable block, and no cheap stack-shape signal separates the two: `culprit` and frame origin fail in both directions on measured real hangs, and in SwiftUI apps the `App.$main` frame is always in-app, keeping the app-vs-system frame mix \"mixed\" on essentially every issue, so that signal carries no information. 'Was the app actively running when captured' is usually not recoverable from the report itself — discriminate with MetricKit instead: `MXHangDiagnostic` call trees for the hang, and `MXAppExitMetric` watchdog-exit counts to see whether users are actually being terminated. If the issue keeps growing across releases, treat it as real regardless of shape.\"\n\n> **`fixed_in_newer_build`:** \"A version split is rollout-exposure-blind — most events sitting on the older build is the normal shape of an incomplete rollout, not proof the bug is fixed. Before closing, verify it actually stopped on the latest build: are there still events there, and is the per-user rate flat-at-zero or *rising* as adoption grows? A flag that fired only because the newest version has little exposure yet, while crashes climb on it, is a live bug — escalate, don't close. Confirm a code change actually touched the crashing path between the two versions.\"\n\nThese are inlined rather than cross-referenced on purpose: `axiom-shipping` (which owns `production-triage.md`) is not shipped to every harness, and a dangling pointer here would drop exactly the safety warnings. `scripts/pre-deploy.ts` asserts these three blocks stay byte-identical to their source in `production-triage.md`.\n\n### Skipped (Malformed or Unclassifiable)\n\n[List errors[] if any — these are issues xcsym couldn't classify]\n\n### Summary\n\n- Total fetched: N\n- Classified: M (N crashes, K hangs)\n- Flagged noise: X (N% of corpus)\n- Real-bug families: Y\n- Skipped: Z\n```\n\n### 6. Route enrichment pointers\n\nFor any issue with non-empty `enrichment[]`:\n- Surface the `enrichment[].note` in the issue's entry under real-bug families\n- If `enrichment[].see` contains `\"axiom-data\"`, add: \"For the fix pattern, read `axiom-data (GRDB suspension / observesSuspensionNotifications / file-protection class)`\"\n\nThe flagship enrichment case: `data_protection_violation` (0xdead10cc) with SQLite/GRDB frames indicates a shared DB lock held across app suspension. The fix is in axiom-data, not axiom-shipping.\n\n## Related\n\n- `axiom-shipping (skills/production-triage.md)` — Full fetch + normalization reference (read this first)\n- `axiom-tools (skills/xcsym-ref.md)` — xcsym subcommand reference including `triage`\n- `crash-analyzer` agent — Single crash file analysis (defer to this when the user has one .ips, not a corpus)\n- `axiom-data` — Fix guidance for 0xdead10cc + DB lock enrichment\n- `axiom-performance (skills/hang-diagnostics.md)` — Deep single-hang investigation when a family warrants it\n\n## Invocation Examples\n\nPrompts that should launch this agent:\n\n\nuser: \"Triage my Sentry crashes\"\nassistant: [Launches triage-analyzer agent]\n\n\n\nuser: \"What are the top crash families in production right now?\"\nassistant: [Launches triage-analyzer agent]\n\n\n\nuser: \"Show me which crashes to fix first from App Store Connect\"\nassistant: [Launches triage-analyzer agent]\n\n\nExplicit command: `/axiom:triage [sentry|asc]`\n\n## Scope\n\nFetches unresolved issues, normalizes them, runs `xcsym triage` to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead.\n" + "content": "\n# Triage Analyzer Agent\n\nYou are an expert at corpus-level production crash and hang triage. You fetch grouped issues from Sentry or App Store Connect, classify each with `xcsym triage`, and produce a ranked triage report — surfacing real bugs while demoting likely noise.\n\n## Core Principle\n\n**Flag, never hide.** Every issue appears in the report. Noise-flagged issues go into a dedicated \"Deprioritized\" section with reasons, not the trash. The ranked real-bug families come first, but nothing is omitted.\n\n## Single-Crash Escape Hatch\n\nIf the user has a **single** crash file (.ips, MetricKit, .crash, .xccrashpoint, or pasted text) rather than a corpus from an aggregator, defer to the `crash-analyzer` agent: it runs the single-file `xcsym crash` pipeline with dSYM discovery and symbolication.\n\nIf the corpus is the **local Xcode Organizer one** — `~/Library/Developer/Xcode/Products//Crashes/Points/*.xccrashpoint` — read `axiom-shipping (skills/testflight-triage.md)`, \"The On-Disk Organizer Corpus\". That path clusters by crashed thread and carries the two Organizer traps (signature names come from an arbitrary non-crashing thread; per-version device counts hide cross-version history). This agent is for corpus triage from Sentry / ASC only.\n\n## Workflow\n\n### 1. Read the production-triage skill\n\nRead `axiom-shipping (skills/production-triage.md)` for the full fetch, normalization, and NormalizedReport schema. It is the authoritative reference for:\n- Sentry API endpoints, cursor pagination (mandatory — never fetch only the first page), and frame mapping (Sentry frames are bottom-up; reverse them)\n- ASC `asc-mcp` tool names and the `frames_unavailable: true` minimal report pattern\n- The exact NormalizedReport JSON shape\n- The flag-never-hide reporting rule\n\n### 2. Fetch and normalize\n\nDetermine the provider from the user's request or command argument:\n\n**Sentry:**\n1. Locate the token per the skill's lookup order (`SENTRY_AUTH_TOKEN` env → `~/.sentryclirc` → project `.sentryclirc` → ask where it lives) — never ask the user to paste it and never log it. Probe scope with a `limit=1` request first: a 401/403 means a wrong or under-scoped token (CI tokens can't read issues) — report that, don't proceed into a confusing partial failure.\n2. `GET /api/0/projects/{org}/{proj}/issues/?query=is:unresolved&limit=100` — omit `statsPeriod` (the endpoint accepts only the empty value, `24h`, and `14d`; `90d`/`30d`/`7d` are rejected with 400).\n3. Follow `Link: rel=\"next\"; results=\"true\"` until exhausted. Log page count when done. Announce any cap you impose.\n4. Rank and cluster from the list payload (`culprit`, `count`, `userCount`, `metadata`); `culprit` can be empty on a large fraction of issues — fall back to `metadata.type` + `metadata.value`. Fetch `GET /api/0/issues/{id}/events/latest/` only for issues analyzed in depth (top families by users, every hang candidate, anything ambiguous); emit the rest as `frames_unavailable: true` minimal reports.\n5. Reverse Sentry frames (bottom-up → top-down). Set `kind: \"hang\"` for \"App Hang\"/\"App Hanging\" issue types.\n\n**App Store Connect:**\n1. Use `metrics_build_diagnostics` to list crash signatures.\n2. Use `metrics_get_diagnostic_logs` to fetch frame detail per issue.\n3. Emit `frames_unavailable: true` for any aggregate without frame data.\n\nWrite each normalized issue as one JSONL line to a temp file (e.g., `/tmp/corpus.jsonl`).\n\n### 3. Run xcsym triage\n\n```bash\nxcsym triage --latest-version --os-floor --min-users < /tmp/corpus.jsonl\n```\n\nOmit flags you don't have values for. The tool exits 0 even when some issues are skipped (malformed lines go to `errors[]`).\n\nParse the TriageResult JSON:\n- `summary.flagged_noise` — how many issues carry at least one noise flag\n- `summary.candidate_families` — estimated real-bug count (mechanical estimate only; semantic merge may revise)\n- `issues[]` — one entry per classified issue\n- `clusters[]` — mechanical groupings by signature\n- `errors[]` — issues that couldn't be classified (report these to the user)\n\n### 4. Semantic family-merge\n\nThe mechanical `cluster_key` is conservative and may over-split (two nil-unwrap clusters with different call sites are the same family) or under-split (`cluster_confidence: low` bags lump unrelated issues under one syscall).\n\n**Merge:** Combine clusters that share `pattern_tag` + overlapping `top_frames` and plausibly represent the same root cause.\n\n**Split `cluster_confidence: low` bags:** Any cluster key containing `|sys:` is a system-frame fallback. Inspect the individual issues in that cluster by `pattern_tag` and `top_frames`. Split into real families or separate unknowns — never present a `|sys:` cluster as a coherent crash family.\n\n### 5. Produce the ranked report\n\n**Report structure:**\n\n```\n## Triage Report — [Provider] — [Date]\n\n**Corpus:** N issues (M crashes, K hangs), N pages fetched\n\n### Real-Bug Families (ranked by users affected)\n\n#### 1. [Family name] — N users, M events\n- **Pattern:** `pattern_tag` (`pattern_confidence`)\n- **Representative issues:** ISSUE-1, ISSUE-2\n- **Top frames:** [list from top_frames]\n- **Root cause hypothesis:** [your interpretation]\n- **Next step:** [specific actionable instruction]\n- **Enrichment:** [if enrichment[] is non-empty, surface the cross-skill pointer here]\n\n[Repeat for each real-bug family]\n\n### Deprioritized as Likely Noise — Review Before Closing\n\n| Issue ID | Title | Users | Noise Class | Deprioritize safety | Reason |\n|---|---|---|---|---|---|\n| ISSUE-X | ... | 68 | anr_suspension_false_positive | medium | main-thread top frames are run-loop park signatures... |\n\nSort **least-safe first** by `noise_flags[].deprioritize_safety` — `low`, then `medium`, then `high` — breaking ties by users descending. The reader is scanning for what they are about to wrongly close.\n\n**No `deprioritize_safety` value licenses closing.** `high` means the deprioritization is comparatively safe, not that the issue is dead. Every row here is review-before-closing, `high` rows included.\n\n**Standing notes:** For every noise class present in the table, reproduce its standing note verbatim. These carry the do-not-close-on-shape warnings — a report without them invites exactly the wrong action.\n\n> **`third_party_or_system_only`:** \"A third-party SDK can crash on a nil or invalid value passed by app code — zero app frames on the crashed thread does not rule out an app-side root cause. Check for app code on other threads or higher in the call chain before dismissing.\"\n\n> **`anr_suspension_false_positive`:** \"Idle-runloop shape says suspension is *likely* — it cannot say the hang was harmless. A watchdog-terminated fatal hang inside a system callout (UIScene, CoreUI, objc-runtime culprits) carries this exact signature while being a real, fixable block, and no cheap stack-shape signal separates the two: `culprit` and frame origin fail in both directions on measured real hangs, and in SwiftUI apps the `App.$main` frame is always in-app, keeping the app-vs-system frame mix \"mixed\" on essentially every issue, so that signal carries no information. 'Was the app actively running when captured' is usually not recoverable from the report itself — discriminate with MetricKit instead: `MXHangDiagnostic` call trees for the hang, and `MXAppExitMetric` watchdog-exit counts to see whether users are actually being terminated. If the issue keeps growing across releases, treat it as real regardless of shape.\"\n\n> **`fixed_in_newer_build`:** \"A version split is rollout-exposure-blind — most events sitting on the older build is the normal shape of an incomplete rollout, not proof the bug is fixed. Before closing, verify it actually stopped on the latest build: are there still events there, and is the per-user rate flat-at-zero or *rising* as adoption grows? A flag that fired only because the newest version has little exposure yet, while crashes climb on it, is a live bug — escalate, don't close. Confirm a code change actually touched the crashing path between the two versions.\"\n\nThese are inlined rather than cross-referenced on purpose: `axiom-shipping` (which owns `production-triage.md`) is not shipped to every harness, and a dangling pointer here would drop exactly the safety warnings. `scripts/pre-deploy.ts` asserts these three blocks stay byte-identical to their source in `production-triage.md`.\n\n### Skipped (Malformed or Unclassifiable)\n\n[List errors[] if any — these are issues xcsym couldn't classify]\n\n### Summary\n\n- Total fetched: N\n- Classified: M (N crashes, K hangs)\n- Flagged noise: X (N% of corpus)\n- Real-bug families: Y\n- Skipped: Z\n```\n\n### 6. Route enrichment pointers\n\nFor any issue with non-empty `enrichment[]`:\n- Surface the `enrichment[].note` in the issue's entry under real-bug families\n- If `enrichment[].see` contains `\"axiom-data\"`, add: \"For the fix pattern, read `axiom-data (GRDB suspension / observesSuspensionNotifications / file-protection class)`\"\n\nThe flagship enrichment case: `data_protection_violation` (0xdead10cc) with SQLite/GRDB frames indicates a shared DB lock held across app suspension. The fix is in axiom-data, not axiom-shipping.\n\n## Related\n\n- `axiom-shipping (skills/production-triage.md)` — Full fetch + normalization reference (read this first)\n- `axiom-tools (skills/xcsym-ref.md)` — xcsym subcommand reference including `triage`\n- `crash-analyzer` agent — Single crash file analysis (defer to this when the user has one .ips, not a corpus)\n- `axiom-data` — Fix guidance for 0xdead10cc + DB lock enrichment\n- `axiom-performance (skills/hang-diagnostics.md)` — Deep single-hang investigation when a family warrants it\n\n## Invocation Examples\n\nPrompts that should launch this agent:\n\n\nuser: \"Triage my Sentry crashes\"\nassistant: [Launches triage-analyzer agent]\n\n\n\nuser: \"What are the top crash families in production right now?\"\nassistant: [Launches triage-analyzer agent]\n\n\n\nuser: \"Show me which crashes to fix first from App Store Connect\"\nassistant: [Launches triage-analyzer agent]\n\n\nExplicit command: `/axiom:triage [sentry|asc]`\n\n## Scope\n\nFetches unresolved issues, normalizes them, runs `xcsym triage` to classify + cluster + flag suspension/idle-runloop noise, merges clusters into root-cause families, and produces a ranked report. For a **single crash file** (.ips, MetricKit, .crash, .xccrashpoint), use the crash-analyzer agent instead; for the **local Organizer corpus** of `.xccrashpoint` bundles, use `axiom-shipping (skills/testflight-triage.md)`.\n" }, "ux-flow-auditor": { "name": "ux-flow-auditor", @@ -47579,14 +47586,14 @@ 27, 2, 36, - 726 + 780 ], "113": [ 3, 20, 9, 40, - 947 + 1108 ], "114": [ 4, @@ -48398,7 +48405,7 @@ 37, 2, 38, - 981 + 1071 ], "230": [ 4, @@ -48517,7 +48524,7 @@ 10, 2, 53, - 907 + 998 ], "247": [ 4, @@ -48549,10 +48556,10 @@ ], "251": [ 4, - 43, + 44, 2, 30, - 588 + 681 ], "252": [ 4, @@ -48615,7 +48622,7 @@ 22, 2, 43, - 947 + 965 ], "261": [ 3, @@ -48698,8 +48705,8 @@ 5, 17, 8, - 196, - 413 + 204, + 477 ], "273": [ 4, @@ -48725,10 +48732,10 @@ }, "averageFieldLength": [ 4.043478260869567, - 19.010869565217398, + 19.0144927536232, 4.239130434782612, - 45.00000000000001, - 713.5905797101448 + 45.028985507246375, + 715.6594202898551 ], "storedFields": { "0": { @@ -50241,7 +50248,7 @@ "skillType": "reference", "source": "axiom", "category": "general", - "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself." + "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself." }, "252": { "skillType": "discipline", @@ -50610,7 +50617,7 @@ "222": 3, "224": 1, "227": 12, - "229": 4, + "229": 6, "230": 2, "244": 1, "249": 2, @@ -50906,6 +50913,14 @@ } } ], + [ + "753", + { + "4": { + "112": 1 + } + } + ], [ "75000", { @@ -51905,6 +51920,7 @@ "237": 1, "239": 5, "241": 1, + "246": 1, "250": 3, "253": 2, "258": 2, @@ -52570,7 +52586,7 @@ "98": 1, "103": 1, "112": 1, - "113": 1, + "113": 8, "115": 1, "137": 2, "141": 9, @@ -53735,7 +53751,7 @@ "233": 1, "237": 1, "240": 2, - "251": 2, + "251": 3, "256": 1, "263": 1, "274": 1 @@ -54340,7 +54356,8 @@ "107": 2, "109": 1, "110": 1, - "113": 7, + "112": 2, + "113": 12, "115": 3, "116": 18, "117": 13, @@ -54585,6 +54602,7 @@ "java", { "4": { + "113": 1, "120": 1, "132": 1, "172": 4, @@ -54794,7 +54812,7 @@ "62": 1, "248": 3, "250": 1, - "272": 3 + "272": 2 } } ], @@ -54928,7 +54946,8 @@ "160": 1, "164": 1, "189": 1, - "202": 2 + "202": 2, + "229": 1 } } ], @@ -55254,6 +55273,7 @@ "13": 3, "36": 1, "215": 2, + "246": 1, "271": 2 } } @@ -55422,6 +55442,7 @@ "106": 1, "107": 4, "108": 1, + "112": 1, "114": 4, "115": 5, "117": 5, @@ -55571,7 +55592,8 @@ "84", { "4": { - "172": 1 + "172": 1, + "229": 1 } } ], @@ -56135,6 +56157,14 @@ } } ], + [ + "970", + { + "4": { + "113": 1 + } + } + ], [ "978307200", { @@ -56515,6 +56545,14 @@ } } ], + [ + "086", + { + "4": { + "113": 1 + } + } + ], [ "0e12af882204c3661fd937f6594c6b5ffc6b8a49", { @@ -57288,7 +57326,10 @@ "35": 1, "82": 1, "120": 1, - "152": 1 + "152": 1, + "246": 1, + "251": 1, + "272": 1 } } ], @@ -57403,6 +57444,14 @@ } } ], + [ + "005", + { + "4": { + "113": 1 + } + } + ], [ "0090", { @@ -57640,8 +57689,8 @@ "4": { "244": 1, "246": 1, - "251": 14, - "272": 13 + "251": 21, + "272": 16 } } ], @@ -58042,6 +58091,8 @@ "95": 10, "107": 1, "110": 1, + "112": 1, + "113": 4, "115": 1, "116": 1, "132": 16, @@ -58057,13 +58108,13 @@ "243": 1, "244": 6, "245": 2, - "246": 29, + "246": 36, "248": 1, "249": 2, "251": 2, "260": 1, "271": 6, - "272": 2, + "272": 3, "273": 4, "274": 45, "275": 4 @@ -58169,6 +58220,7 @@ "181": 1, "227": 1, "228": 2, + "229": 2, "241": 1, "242": 1, "243": 5, @@ -58176,8 +58228,8 @@ "245": 16, "246": 10, "249": 1, - "251": 40, - "272": 1 + "251": 46, + "272": 4 } } ], @@ -58189,12 +58241,20 @@ } } ], + [ + "xcstringstool", + { + "4": { + "113": 7 + } + } + ], [ "xcstrings", { "4": { "112": 12, - "113": 18 + "113": 22 } } ], @@ -58776,7 +58836,7 @@ "107": 5, "108": 9, "112": 16, - "113": 35, + "113": 38, "114": 6, "115": 3, "117": 3, @@ -58836,7 +58896,7 @@ "225": 14, "226": 16, "228": 8, - "229": 4, + "229": 5, "230": 1, "232": 2, "233": 1, @@ -58852,7 +58912,7 @@ "248": 1, "249": 1, "250": 16, - "251": 4, + "251": 5, "253": 5, "260": 6, "263": 4, @@ -58862,7 +58922,7 @@ "268": 5, "270": 4, "271": 28, - "272": 4, + "272": 5, "273": 97, "274": 105, "275": 76 @@ -59685,7 +59745,8 @@ "115": 4, "116": 2, "141": 1, - "214": 1 + "214": 1, + "229": 1 } } ], @@ -59827,6 +59888,7 @@ "222": 1, "231": 1, "244": 1, + "251": 1, "260": 1, "272": 10 } @@ -59917,6 +59979,14 @@ } } ], + [ + "447", + { + "4": { + "113": 2 + } + } + ], [ "440", { @@ -61874,6 +61944,14 @@ } } ], + [ + "531", + { + "4": { + "113": 1 + } + } + ], [ "576", { @@ -62166,6 +62244,7 @@ "4": { "15": 1, "94": 2, + "112": 1, "120": 1, "205": 1, "206": 1, @@ -63036,6 +63115,7 @@ "212": 1, "217": 1, "226": 1, + "229": 1, "230": 1 } } @@ -63916,7 +63996,7 @@ "107": 2, "108": 1, "112": 2, - "113": 4, + "113": 5, "115": 4, "116": 1, "117": 7, @@ -64464,7 +64544,7 @@ "222": 1, "236": 1, "259": 1, - "260": 1, + "260": 2, "263": 1, "273": 6 } @@ -64499,7 +64579,7 @@ "102": 1, "103": 1, "112": 1, - "113": 2, + "113": 3, "119": 1, "123": 1, "131": 1, @@ -64557,6 +64637,7 @@ "94": 2, "98": 1, "102": 3, + "113": 1, "115": 1, "119": 3, "123": 1, @@ -64581,7 +64662,7 @@ "223": 1, "225": 1, "227": 1, - "229": 1, + "229": 2, "230": 3, "234": 2, "240": 1, @@ -64887,9 +64968,11 @@ "232": 1, "239": 6, "244": 3, + "246": 1, + "251": 2, "253": 1, "258": 2, - "272": 2, + "272": 3, "273": 1, "275": 1 } @@ -65144,6 +65227,7 @@ "108": 8, "109": 2, "110": 1, + "113": 2, "114": 1, "116": 2, "117": 5, @@ -65252,7 +65336,7 @@ "192": 2, "215": 1, "244": 5, - "246": 9, + "246": 10, "274": 1 } } @@ -65722,6 +65806,14 @@ } } ], + [ + "globstar", + { + "4": { + "113": 1 + } + } + ], [ "glob", { @@ -65877,6 +65969,7 @@ "233": 18, "234": 1, "240": 8, + "246": 1, "247": 9, "260": 2, "266": 5, @@ -66591,6 +66684,7 @@ "241": 1, "244": 1, "249": 6, + "251": 1, "256": 3, "260": 1, "262": 3, @@ -67282,7 +67376,7 @@ "221": 4, "223": 2, "226": 1, - "229": 6, + "229": 5, "232": 2, "235": 1, "240": 1, @@ -67290,7 +67384,7 @@ "248": 2, "249": 1, "250": 3, - "251": 1, + "251": 2, "256": 1, "263": 1, "273": 2, @@ -67353,6 +67447,7 @@ "25": 7, "43": 1, "107": 1, + "112": 3, "115": 1, "165": 1, "166": 1, @@ -69015,6 +69110,7 @@ "45": 6, "63": 1, "112": 3, + "113": 1, "244": 2 } } @@ -69742,6 +69838,7 @@ "99": 2, "100": 2, "112": 1, + "113": 1, "118": 2, "119": 3, "124": 1, @@ -69900,8 +69997,7 @@ "112": 1, "155": 1, "182": 1, - "206": 2, - "251": 1 + "206": 2 }, "3": { "80": 1, @@ -69914,7 +70010,7 @@ "208": 1, "223": 1, "225": 1, - "272": 5 + "272": 4 }, "4": { "0": 1, @@ -70085,7 +70181,7 @@ "244": 1, "246": 3, "247": 1, - "251": 2, + "251": 1, "254": 2, "257": 6, "259": 2, @@ -70099,7 +70195,7 @@ "269": 1, "270": 2, "271": 3, - "272": 6, + "272": 5, "273": 20, "274": 4, "275": 9 @@ -70203,6 +70299,7 @@ "263": 1, "266": 1, "268": 1, + "272": 1, "273": 3, "274": 1 } @@ -70395,12 +70492,12 @@ "232": 13, "233": 4, "244": 1, - "246": 1, + "246": 2, "247": 1, "252": 1, "257": 2, "259": 1, - "260": 11, + "260": 12, "266": 4 } } @@ -70716,7 +70813,7 @@ "8": 1, "9": 1, "63": 3, - "112": 1, + "112": 2, "221": 1 } } @@ -70813,6 +70910,7 @@ "kbps", { "4": { + "113": 4, "141": 2, "210": 2, "244": 1 @@ -71479,7 +71577,7 @@ "223": 3, "226": 2, "227": 2, - "229": 17, + "229": 19, "230": 4, "231": 1, "232": 3, @@ -71750,7 +71848,7 @@ "223": 4, "226": 1, "228": 2, - "229": 3, + "229": 4, "230": 5, "231": 4, "233": 1, @@ -71762,7 +71860,7 @@ "244": 1, "258": 1, "259": 1, - "260": 3, + "260": 4, "261": 1, "262": 1, "264": 1, @@ -71848,7 +71946,7 @@ "108": 2, "109": 1, "110": 2, - "113": 4, + "113": 5, "114": 4, "115": 1, "117": 1, @@ -71908,7 +72006,7 @@ "224": 1, "226": 2, "228": 1, - "229": 19, + "229": 20, "230": 1, "231": 3, "237": 3, @@ -72212,7 +72310,8 @@ "127": 4, "128": 2, "129": 4, - "237": 2 + "237": 2, + "246": 1 } } ], @@ -72274,7 +72373,8 @@ "102": 14, "104": 2, "110": 2, - "113": 3, + "112": 1, + "113": 9, "115": 4, "116": 2, "118": 1, @@ -72471,8 +72571,8 @@ "107": 16, "109": 1, "110": 1, - "112": 1, - "113": 22, + "112": 3, + "113": 38, "114": 67, "115": 22, "116": 22, @@ -73824,6 +73924,7 @@ { "4": { "78": 1, + "112": 1, "184": 1, "214": 1, "230": 1 @@ -73890,11 +73991,11 @@ "197": 5, "203": 1, "228": 3, - "229": 3, + "229": 4, "237": 1, "241": 1, "244": 2, - "246": 16, + "246": 17, "260": 1, "271": 19, "272": 1, @@ -74032,7 +74133,7 @@ "224": 11, "226": 2, "227": 6, - "229": 24, + "229": 26, "230": 28, "231": 12, "232": 1, @@ -74172,12 +74273,13 @@ "220": 2, "221": 5, "223": 3, - "229": 1, + "229": 3, "233": 4, "234": 3, "237": 1, "241": 7, "244": 9, + "246": 2, "259": 54, "267": 7, "268": 3, @@ -74225,6 +74327,14 @@ } } ], + [ + "hoists", + { + "4": { + "113": 1 + } + } + ], [ "hoist", { @@ -74769,6 +74879,7 @@ "87": 1, "94": 1, "109": 1, + "112": 1, "113": 1, "115": 2, "121": 1, @@ -74806,7 +74917,7 @@ { "3": { "193": 1, - "272": 2 + "272": 3 }, "4": { "0": 1, @@ -74854,14 +74965,16 @@ "223": 1, "225": 1, "227": 5, + "229": 1, "230": 1, - "246": 1, + "246": 2, + "251": 1, "255": 2, "256": 3, "260": 2, "262": 1, "263": 1, - "272": 3, + "272": 4, "274": 1, "275": 1 } @@ -75172,7 +75285,6 @@ "227": 14, "237": 1, "239": 3, - "251": 1, "255": 7, "256": 7, "260": 5 @@ -75479,6 +75591,7 @@ "238": 1, "239": 1, "241": 1, + "246": 1, "252": 8, "253": 13, "255": 1, @@ -76405,6 +76518,7 @@ "220": 5, "221": 1, "224": 13, + "229": 1, "230": 4, "244": 1, "247": 3, @@ -76476,7 +76590,7 @@ "headlessly", { "4": { - "246": 1 + "246": 2 } } ], @@ -76491,6 +76605,7 @@ }, "4": { "25": 1, + "113": 1, "169": 1, "181": 2, "186": 1, @@ -76533,7 +76648,8 @@ "head", { "3": { - "125": 1 + "125": 1, + "272": 1 }, "4": { "0": 1, @@ -76548,6 +76664,7 @@ "58": 1, "93": 1, "95": 1, + "112": 1, "115": 1, "125": 16, "128": 3, @@ -76570,7 +76687,7 @@ "257": 1, "260": 3, "263": 5, - "272": 2, + "272": 3, "273": 1 } } @@ -76940,6 +77057,7 @@ "214": 23, "215": 10, "239": 1, + "246": 1, "249": 5, "250": 15 } @@ -77455,6 +77573,7 @@ "108": 13, "109": 1, "111": 3, + "113": 1, "114": 6, "115": 1, "116": 1, @@ -77649,6 +77768,7 @@ "256": 1, "257": 1, "258": 9, + "260": 2, "261": 39, "262": 49, "263": 77, @@ -77875,6 +77995,7 @@ "71": 1, "85": 1, "106": 1, + "113": 2, "137": 1, "155": 1, "157": 1, @@ -77884,6 +78005,7 @@ "215": 2, "222": 1, "224": 1, + "229": 1, "237": 1, "242": 1, "260": 1 @@ -78085,9 +78207,6 @@ [ "hav", { - "1": { - "251": 1 - }, "2": { "47": 1 }, @@ -78102,7 +78221,6 @@ "170": 1, "173": 1, "214": 1, - "251": 1, "268": 1 } } @@ -78267,6 +78385,7 @@ "165": 1, "237": 1, "244": 4, + "246": 1, "249": 1, "251": 1 } @@ -78301,6 +78420,7 @@ "4": { "13": 1, "63": 1, + "113": 1, "122": 1 } } @@ -78348,6 +78468,14 @@ } } ], + [ + "hardcodes", + { + "4": { + "113": 1 + } + } + ], [ "hardcode", { @@ -78462,7 +78590,7 @@ "221": 1, "227": 1, "228": 1, - "229": 3, + "229": 4, "237": 3, "243": 1, "244": 4, @@ -78713,7 +78841,7 @@ "160": 1, "176": 2, "223": 2, - "229": 6, + "229": 7, "231": 3, "267": 1, "271": 1 @@ -78922,7 +79050,8 @@ }, "4": { "77": 1, - "229": 30 + "229": 36, + "246": 2 } } ], @@ -79583,6 +79712,7 @@ "104": 1, "106": 1, "109": 1, + "113": 12, "117": 1, "122": 2, "125": 2, @@ -79628,7 +79758,7 @@ "223": 10, "225": 1, "227": 2, - "229": 1, + "229": 3, "230": 1, "236": 1, "239": 3, @@ -79649,7 +79779,6 @@ "hid", { "1": { - "251": 1, "272": 1 }, "2": { @@ -79682,7 +79811,6 @@ "230": 1, "237": 1, "238": 1, - "251": 2, "272": 2 } } @@ -79736,16 +79864,19 @@ "224": 4, "225": 1, "226": 6, + "229": 1, "233": 13, "234": 1, "238": 2, "240": 7, "241": 1, + "251": 1, "255": 1, "259": 1, "263": 1, "266": 1, - "267": 1 + "267": 1, + "272": 1 } } ], @@ -80127,7 +80258,7 @@ "224": 1, "225": 9, "227": 5, - "229": 5, + "229": 6, "230": 2, "231": 2, "233": 13, @@ -80137,6 +80268,7 @@ "242": 1, "246": 1, "247": 1, + "251": 15, "254": 1, "255": 1, "260": 4, @@ -80187,7 +80319,7 @@ "223": 6, "226": 1, "227": 1, - "229": 3, + "229": 4, "230": 2, "231": 1, "257": 1, @@ -81146,6 +81278,7 @@ "80": 2, "95": 2, "108": 1, + "113": 1, "114": 1, "118": 1, "119": 5, @@ -81760,7 +81893,7 @@ "225": 7, "226": 32, "228": 3, - "229": 4, + "229": 5, "232": 5, "233": 1, "234": 1, @@ -81772,7 +81905,7 @@ "243": 6, "244": 12, "245": 3, - "246": 8, + "246": 9, "248": 13, "249": 2, "250": 20, @@ -81782,7 +81915,7 @@ "254": 33, "257": 1, "259": 2, - "260": 3, + "260": 6, "261": 5, "262": 3, "263": 13, @@ -82101,7 +82234,7 @@ "78": 1, "94": 1, "106": 2, - "113": 1, + "113": 2, "118": 2, "119": 1, "120": 1, @@ -82184,7 +82317,7 @@ "66": 2, "67": 3, "68": 1, - "113": 1, + "113": 2, "119": 1, "187": 1, "189": 1, @@ -82397,7 +82530,7 @@ "106": 2, "107": 2, "108": 1, - "113": 1, + "113": 2, "119": 3, "120": 2, "121": 1, @@ -82456,6 +82589,7 @@ "243": 1, "249": 1, "250": 4, + "251": 1, "255": 2, "257": 1, "260": 1, @@ -82741,6 +82875,7 @@ "4": { "29": 1, "48": 1, + "113": 1, "217": 2 } } @@ -83448,7 +83583,7 @@ "108": 10, "109": 1, "110": 1, - "113": 1, + "113": 2, "114": 5, "116": 2, "117": 7, @@ -83538,7 +83673,7 @@ "244": 4, "245": 1, "246": 1, - "251": 1, + "251": 2, "255": 2, "256": 1, "257": 3, @@ -83785,13 +83920,13 @@ "243": 4, "244": 40, "245": 30, - "251": 6, + "251": 8, "255": 5, "256": 1, "257": 22, "260": 1, "266": 1, - "272": 14 + "272": 15 } } ], @@ -84004,7 +84139,7 @@ "63": 4, "67": 1, "94": 1, - "113": 3, + "113": 6, "130": 20, "134": 4, "136": 18, @@ -84229,7 +84364,7 @@ "221": 24, "222": 2, "225": 3, - "229": 19, + "229": 29, "230": 17, "231": 2, "232": 2, @@ -84577,6 +84712,7 @@ "95": 1, "97": 1, "98": 8, + "113": 4, "121": 8, "122": 4, "124": 2, @@ -84643,8 +84779,8 @@ "35": 4, "42": 1, "94": 9, - "112": 2, - "113": 14, + "112": 3, + "113": 17, "114": 1, "125": 1, "171": 1, @@ -84928,6 +85064,7 @@ "71": 3, "72": 2, "107": 1, + "113": 1, "114": 1, "115": 2, "116": 2, @@ -85946,6 +86083,7 @@ "159": 1, "164": 1, "215": 1, + "246": 3, "257": 6 } } @@ -86033,6 +86171,7 @@ "93": 4, "94": 12, "108": 2, + "113": 1, "118": 3, "119": 2, "120": 2, @@ -86274,8 +86413,8 @@ "45": 7, "63": 1, "94": 8, - "112": 4, - "113": 5, + "112": 6, + "113": 11, "243": 1, "244": 1, "245": 1, @@ -86326,7 +86465,7 @@ "106": 25, "107": 17, "111": 1, - "113": 1, + "113": 3, "152": 37, "172": 1, "181": 1, @@ -86345,7 +86484,7 @@ "267": 2, "269": 7, "270": 3, - "272": 2, + "272": 1, "273": 6 } } @@ -86402,7 +86541,7 @@ "99": 2, "106": 67, "107": 20, - "113": 8, + "113": 19, "114": 3, "116": 1, "118": 1, @@ -86564,7 +86703,6 @@ "243": 1, "244": 1, "245": 1, - "246": 1, "272": 2 } } @@ -88019,6 +88157,7 @@ "244": 2, "246": 1, "249": 2, + "251": 1, "253": 1, "254": 2, "260": 1, @@ -88144,8 +88283,8 @@ "107": 5, "110": 5, "111": 3, - "112": 9, - "113": 2, + "112": 10, + "113": 8, "115": 2, "117": 3, "118": 4, @@ -88233,7 +88372,7 @@ "225": 4, "226": 8, "227": 3, - "229": 15, + "229": 17, "230": 6, "231": 4, "232": 7, @@ -88248,7 +88387,7 @@ "246": 5, "247": 1, "250": 6, - "251": 3, + "251": 7, "252": 2, "253": 2, "254": 4, @@ -88266,6 +88405,7 @@ "269": 3, "270": 11, "271": 1, + "272": 2, "273": 2, "274": 3, "275": 6 @@ -88398,14 +88538,14 @@ "224": 1, "226": 1, "227": 1, - "229": 4, + "229": 5, "230": 1, "231": 4, "235": 1, "236": 1, "237": 2, "239": 6, - "246": 1, + "246": 2, "254": 1, "255": 1, "260": 2, @@ -89183,7 +89323,7 @@ "110": 2, "111": 1, "112": 2, - "113": 6, + "113": 7, "114": 1, "115": 1, "117": 1, @@ -89276,7 +89416,7 @@ "226": 21, "227": 4, "228": 3, - "229": 10, + "229": 11, "230": 14, "231": 4, "232": 10, @@ -89293,7 +89433,7 @@ "246": 3, "249": 1, "250": 3, - "251": 3, + "251": 4, "252": 1, "254": 7, "255": 2, @@ -89894,6 +90034,7 @@ "62": 1, "70": 1, "97": 1, + "113": 1, "116": 2, "121": 1, "124": 2, @@ -89917,7 +90058,7 @@ "246": 5, "249": 10, "250": 14, - "251": 9, + "251": 10, "274": 2 } } @@ -90087,7 +90228,7 @@ "221": 1, "223": 2, "227": 1, - "229": 1, + "229": 2, "231": 1, "232": 2, "233": 1, @@ -90747,6 +90888,7 @@ "210": 1, "229": 1, "233": 1, + "246": 1, "260": 2 } } @@ -91300,6 +91442,7 @@ "239": 3, "241": 2, "244": 1, + "246": 1, "249": 1, "260": 9, "273": 1, @@ -91434,7 +91577,7 @@ "92": 1, "107": 1, "112": 1, - "113": 5, + "113": 7, "188": 1, "199": 2, "223": 3, @@ -91492,7 +91635,7 @@ "92": 3, "103": 1, "112": 1, - "113": 4, + "113": 5, "151": 2, "156": 2, "188": 2, @@ -91537,7 +91680,7 @@ "56": 1, "92": 1, "112": 1, - "113": 9, + "113": 11, "141": 1, "156": 1, "196": 2, @@ -92323,7 +92466,7 @@ "236": 1, "237": 1, "241": 1, - "246": 1, + "246": 2, "250": 1, "257": 1, "259": 3, @@ -92406,7 +92549,7 @@ "108": 1, "110": 2, "112": 1, - "113": 2, + "113": 4, "115": 2, "125": 1, "135": 1, @@ -93364,6 +93507,7 @@ "108": 2, "111": 1, "112": 1, + "113": 1, "119": 3, "120": 2, "133": 2, @@ -93793,7 +93937,8 @@ "103": 1, "106": 2, "107": 1, - "112": 1, + "112": 2, + "113": 1, "115": 3, "117": 1, "118": 1, @@ -93849,7 +93994,7 @@ "246": 2, "248": 1, "249": 1, - "251": 1, + "251": 2, "253": 1, "260": 2, "264": 1, @@ -93905,6 +94050,8 @@ "97": 1, "103": 1, "107": 1, + "112": 2, + "113": 1, "119": 1, "139": 1, "145": 1, @@ -94269,6 +94416,7 @@ "72": 2, "88": 2, "89": 1, + "113": 1, "139": 1, "144": 1, "157": 1, @@ -94658,6 +94806,7 @@ "62": 1, "74": 1, "94": 1, + "112": 1, "116": 1, "129": 1, "132": 4, @@ -95003,6 +95152,7 @@ "100": 1, "110": 1, "111": 1, + "113": 1, "119": 1, "123": 1, "124": 1, @@ -95917,6 +96067,7 @@ "61": 1, "73": 1, "107": 3, + "113": 1, "136": 1, "137": 1, "143": 1, @@ -95975,8 +96126,9 @@ "178": 2, "183": 2, "209": 1, + "229": 1, "244": 1, - "246": 2, + "246": 7, "255": 2, "261": 2, "266": 2 @@ -96069,6 +96221,7 @@ "44": 1, "45": 1, "106": 1, + "112": 1, "120": 3, "126": 1, "132": 1, @@ -96566,6 +96719,7 @@ "4": { "0": 1, "1": 4, + "113": 1, "158": 1, "209": 1, "210": 1, @@ -97671,8 +97825,8 @@ "108": 7, "109": 1, "110": 1, - "112": 2, - "113": 17, + "112": 3, + "113": 18, "114": 2, "116": 3, "117": 2, @@ -97733,7 +97887,7 @@ "225": 5, "226": 12, "227": 12, - "229": 18, + "229": 21, "230": 8, "231": 7, "232": 1, @@ -97748,7 +97902,7 @@ "244": 42, "245": 20, "246": 1, - "251": 4, + "251": 6, "253": 3, "254": 2, "255": 2, @@ -97761,7 +97915,7 @@ "265": 11, "266": 15, "270": 1, - "272": 23, + "272": 24, "273": 2 } } @@ -97975,7 +98129,7 @@ "226": 20, "227": 1, "228": 2, - "229": 5, + "229": 6, "230": 6, "231": 1, "232": 12, @@ -97990,7 +98144,7 @@ "241": 1, "244": 12, "245": 1, - "246": 3, + "246": 5, "249": 2, "250": 2, "251": 1, @@ -98011,7 +98165,7 @@ "268": 1, "270": 3, "271": 8, - "272": 1, + "272": 2, "273": 3, "274": 2, "275": 1 @@ -98748,7 +98902,7 @@ "226": 29, "227": 17, "228": 5, - "229": 9, + "229": 10, "230": 14, "231": 7, "232": 20, @@ -98979,8 +99133,8 @@ "244": 1, "246": 7, "248": 3, - "251": 3, - "272": 3 + "251": 11, + "272": 6 } } ], @@ -99368,7 +99522,7 @@ "221": 33, "222": 1, "224": 4, - "229": 12, + "229": 18, "230": 13, "232": 4, "233": 13, @@ -99376,7 +99530,7 @@ "236": 4, "238": 5, "240": 22, - "246": 1, + "246": 2, "251": 1, "252": 2, "253": 1, @@ -99510,7 +99664,7 @@ "108": 1, "109": 1, "111": 3, - "113": 4, + "113": 6, "114": 1, "115": 10, "116": 1, @@ -99597,7 +99751,7 @@ "224": 1, "226": 1, "227": 7, - "229": 8, + "229": 9, "230": 8, "231": 2, "232": 2, @@ -99609,7 +99763,7 @@ "240": 3, "241": 3, "244": 2, - "246": 7, + "246": 8, "247": 1, "248": 1, "249": 7, @@ -100743,6 +100897,7 @@ "174": 1, "185": 2, "186": 1, + "229": 1, "273": 1 } } @@ -100827,7 +100982,7 @@ "100": 1, "106": 1, "220": 1, - "229": 1, + "229": 2, "247": 1, "264": 1, "265": 2 @@ -101476,6 +101631,7 @@ "103": 3, "106": 1, "107": 7, + "113": 1, "114": 3, "115": 3, "116": 1, @@ -101753,6 +101909,8 @@ "95": 2, "107": 3, "108": 1, + "112": 2, + "113": 2, "115": 4, "116": 4, "124": 1, @@ -101791,7 +101949,7 @@ "243": 2, "244": 4, "245": 4, - "246": 10, + "246": 12, "248": 9, "249": 2, "250": 7, @@ -102309,6 +102467,7 @@ "218": 1, "225": 1, "227": 2, + "229": 2, "234": 1, "239": 1, "240": 1, @@ -102383,7 +102542,7 @@ "221": 37, "222": 1, "228": 1, - "229": 57, + "229": 76, "230": 10, "231": 12, "232": 5, @@ -102433,7 +102592,7 @@ "221": 6, "222": 1, "225": 3, - "229": 26, + "229": 27, "233": 4, "235": 2, "239": 3, @@ -102810,7 +102969,7 @@ "108": 4, "109": 2, "112": 2, - "113": 3, + "113": 4, "114": 3, "115": 4, "117": 4, @@ -102886,7 +103045,7 @@ "224": 2, "225": 1, "226": 7, - "229": 6, + "229": 7, "230": 9, "231": 2, "232": 7, @@ -103192,7 +103351,7 @@ "221": 21, "226": 7, "227": 1, - "229": 6, + "229": 10, "230": 6, "232": 10, "233": 8, @@ -104030,7 +104189,8 @@ "104": 1, "106": 4, "108": 3, - "112": 1, + "112": 2, + "113": 2, "118": 2, "119": 1, "120": 3, @@ -104110,6 +104270,7 @@ "247": 3, "249": 4, "250": 3, + "251": 1, "255": 1, "257": 3, "260": 3, @@ -104257,7 +104418,7 @@ "108": 17, "110": 1, "112": 12, - "113": 6, + "113": 8, "114": 7, "115": 12, "116": 2, @@ -104361,7 +104522,7 @@ "225": 12, "226": 9, "227": 6, - "229": 1, + "229": 2, "230": 4, "231": 2, "232": 9, @@ -104680,7 +104841,7 @@ "107": 1, "108": 2, "112": 1, - "113": 1, + "113": 2, "115": 1, "119": 2, "122": 4, @@ -104737,7 +104898,7 @@ "242": 2, "244": 1, "249": 2, - "251": 1, + "251": 3, "254": 1, "256": 1, "258": 1, @@ -104799,6 +104960,7 @@ "215": 2, "216": 1, "217": 2, + "229": 1, "237": 1, "239": 2, "254": 2, @@ -104998,7 +105160,7 @@ "224": 2, "226": 2, "227": 4, - "229": 13, + "229": 14, "230": 4, "231": 2, "232": 3, @@ -105013,6 +105175,7 @@ "245": 1, "246": 1, "248": 1, + "251": 1, "254": 3, "257": 7, "258": 1, @@ -105136,6 +105299,7 @@ "221": 1, "226": 1, "228": 1, + "229": 1, "230": 2, "231": 4, "233": 2, @@ -105874,6 +106038,14 @@ } } ], + [ + "rt", + { + "4": { + "112": 4 + } + } + ], [ "rtp", { @@ -106210,7 +106382,7 @@ "105": 1, "108": 1, "112": 1, - "113": 3, + "113": 4, "115": 1, "116": 2, "119": 4, @@ -106665,6 +106837,7 @@ "100": 1, "110": 1, "111": 4, + "113": 1, "116": 1, "119": 1, "120": 2, @@ -106747,7 +106920,8 @@ "runtimes", { "4": { - "19": 1 + "19": 1, + "112": 3 } } ], @@ -106824,7 +106998,7 @@ "109": 2, "110": 1, "111": 1, - "112": 1, + "112": 5, "113": 1, "121": 1, "126": 1, @@ -106863,13 +107037,14 @@ "220": 1, "223": 4, "226": 3, - "229": 3, + "229": 5, "230": 3, "236": 2, "237": 3, "239": 2, "248": 2, "250": 1, + "251": 1, "253": 2, "254": 2, "260": 3, @@ -107002,7 +107177,7 @@ "108": 3, "110": 1, "112": 5, - "113": 1, + "113": 2, "114": 1, "115": 3, "116": 1, @@ -107082,7 +107257,7 @@ "243": 5, "244": 40, "245": 3, - "246": 7, + "246": 8, "247": 5, "248": 3, "249": 5, @@ -107249,7 +107424,7 @@ "223": 6, "225": 2, "226": 3, - "229": 7, + "229": 8, "230": 3, "236": 1, "237": 2, @@ -108764,7 +108939,7 @@ "246": 1, "249": 1, "250": 1, - "251": 1, + "251": 2, "252": 1, "256": 1, "260": 1, @@ -109285,7 +109460,7 @@ "226": 24, "227": 5, "228": 1, - "229": 11, + "229": 14, "230": 2, "231": 1, "232": 1, @@ -109295,6 +109470,7 @@ "239": 4, "240": 5, "244": 7, + "246": 2, "247": 4, "250": 1, "251": 1, @@ -109302,6 +109478,7 @@ "253": 3, "254": 1, "257": 4, + "260": 1, "261": 9, "262": 18, "263": 21, @@ -109408,6 +109585,7 @@ "160": 2, "179": 1, "186": 1, + "229": 1, "230": 2, "231": 1, "240": 2, @@ -109476,6 +109654,7 @@ "104": 1, "106": 1, "107": 1, + "112": 1, "120": 1, "130": 13, "132": 1, @@ -109614,7 +109793,8 @@ { "4": { "108": 1, - "144": 1 + "144": 1, + "246": 3 } } ], @@ -109628,7 +109808,9 @@ "145": 1, "220": 1, "229": 2, + "246": 1, "253": 1, + "260": 1, "261": 3 } } @@ -109640,7 +109822,8 @@ "64": 2, "109": 1, "125": 1, - "180": 1 + "180": 1, + "246": 1 } } ], @@ -109680,7 +109863,7 @@ "227": 9, "229": 1, "243": 1, - "246": 2, + "246": 8, "253": 2, "255": 1, "260": 2, @@ -110114,6 +110297,7 @@ "94": 1, "100": 2, "112": 3, + "113": 1, "115": 1, "157": 2, "164": 1, @@ -110308,6 +110492,7 @@ "221": 3, "222": 2, "223": 7, + "229": 1, "232": 5, "233": 9, "234": 8, @@ -110386,7 +110571,7 @@ "230": 19, "237": 1, "239": 8, - "246": 1, + "246": 2, "262": 2, "263": 1, "271": 1 @@ -110461,12 +110646,14 @@ "239": 9, "240": 4, "244": 2, + "251": 3, "252": 2, "262": 2, "263": 2, "266": 1, "268": 1, - "271": 2 + "271": 2, + "272": 2 } } ], @@ -110613,7 +110800,7 @@ "241": 1, "242": 1, "244": 1, - "246": 1, + "246": 2, "250": 2, "261": 1, "274": 1, @@ -110855,6 +111042,7 @@ { "4": { "93": 1, + "113": 1, "239": 1 } } @@ -111815,11 +112003,11 @@ "240": 2, "241": 1, "244": 13, - "246": 1, + "246": 2, "251": 2, "254": 8, "257": 2, - "260": 10, + "260": 13, "261": 117, "262": 121, "263": 163, @@ -112353,7 +112541,7 @@ "4": { "36": 1, "41": 1, - "113": 1, + "113": 2, "172": 1, "223": 1, "246": 1, @@ -112504,14 +112692,6 @@ } } ], - [ - "revisions", - { - "4": { - "263": 2 - } - } - ], [ "revision3", { @@ -112521,6 +112701,15 @@ } } ], + [ + "revisions", + { + "4": { + "113": 1, + "263": 2 + } + } + ], [ "revision", { @@ -113113,7 +113302,8 @@ "36": 6, "37": 2, "48": 1, - "54": 3 + "54": 3, + "113": 1 } } ], @@ -113276,7 +113466,7 @@ "238": 3, "239": 3, "240": 3, - "246": 3, + "246": 4, "251": 1, "255": 2, "256": 2, @@ -113473,6 +113663,7 @@ "225": 1, "226": 4, "228": 1, + "229": 2, "230": 1, "231": 1, "232": 6, @@ -113509,6 +113700,7 @@ "41": 1, "47": 1, "107": 1, + "113": 1, "129": 2, "144": 2, "153": 1, @@ -113677,6 +113869,7 @@ "221": 1, "223": 1, "226": 1, + "229": 1, "230": 1, "232": 2, "240": 2, @@ -114107,6 +114300,7 @@ "101": 3, "105": 1, "107": 1, + "112": 1, "115": 3, "117": 1, "122": 1, @@ -114172,6 +114366,7 @@ "105": 2, "106": 2, "108": 1, + "112": 1, "115": 11, "116": 4, "117": 15, @@ -114865,6 +115060,7 @@ "244": 3, "245": 2, "246": 1, + "251": 1, "254": 6, "261": 1, "263": 1, @@ -115519,7 +115715,7 @@ "57": 2, "106": 6, "221": 3, - "246": 1, + "246": 2, "257": 1, "261": 3, "269": 3 @@ -115563,7 +115759,7 @@ "227": 6, "229": 3, "230": 2, - "246": 1, + "246": 2, "259": 1, "262": 2, "263": 6 @@ -115940,6 +116136,7 @@ "219": 15, "232": 4, "242": 1, + "246": 1, "257": 1, "270": 1 } @@ -117268,7 +117465,8 @@ "repair", { "4": { - "11": 1 + "11": 1, + "113": 1 } } ], @@ -117431,6 +117629,7 @@ "217": 2, "218": 1, "222": 4, + "229": 1, "231": 1, "241": 7, "244": 1, @@ -117945,11 +118144,12 @@ "209": 1, "215": 2, "225": 2, + "229": 6, "230": 2, "232": 1, "235": 1, "241": 2, - "246": 1, + "246": 3, "249": 1, "271": 1, "273": 1, @@ -118052,7 +118252,7 @@ "221": 1, "225": 6, "226": 5, - "229": 4, + "229": 6, "230": 5, "231": 6, "236": 2, @@ -118061,7 +118261,7 @@ "246": 3, "249": 5, "250": 5, - "251": 3, + "251": 4, "257": 1, "260": 1, "263": 1, @@ -118691,6 +118891,7 @@ "103": 1, "107": 7, "111": 2, + "112": 1, "114": 19, "115": 1, "117": 1, @@ -119427,9 +119628,6 @@ [ "real", { - "1": { - "251": 1 - }, "3": { "21": 1, "22": 1, @@ -119530,6 +119728,7 @@ "108": 3, "110": 3, "112": 4, + "113": 1, "115": 6, "119": 2, "120": 4, @@ -119606,7 +119805,7 @@ "246": 2, "249": 1, "250": 2, - "251": 3, + "251": 1, "252": 1, "253": 1, "254": 1, @@ -119982,6 +120181,7 @@ "105": 1, "106": 3, "107": 1, + "113": 2, "122": 1, "133": 2, "134": 2, @@ -120017,7 +120217,7 @@ "222": 1, "223": 2, "226": 5, - "229": 2, + "229": 6, "230": 2, "231": 1, "232": 1, @@ -120026,7 +120226,7 @@ "239": 4, "240": 1, "244": 2, - "246": 1, + "246": 2, "249": 1, "251": 1, "260": 2, @@ -120142,7 +120342,8 @@ "107": 4, "108": 1, "109": 1, - "112": 2, + "112": 5, + "113": 1, "114": 2, "117": 1, "118": 2, @@ -120227,7 +120428,7 @@ "226": 8, "227": 2, "228": 1, - "229": 6, + "229": 8, "230": 13, "231": 7, "232": 4, @@ -120239,11 +120440,11 @@ "242": 4, "243": 1, "244": 5, - "246": 5, + "246": 6, "248": 5, "249": 3, "250": 6, - "251": 1, + "251": 2, "252": 1, "253": 1, "254": 5, @@ -121674,7 +121875,7 @@ "110": 1, "111": 2, "112": 6, - "113": 1, + "113": 4, "115": 7, "117": 2, "118": 1, @@ -121765,6 +121966,7 @@ "225": 6, "226": 22, "228": 2, + "229": 2, "230": 2, "231": 2, "232": 4, @@ -121787,7 +121989,7 @@ "257": 1, "258": 4, "259": 4, - "260": 1, + "260": 2, "261": 1, "262": 2, "264": 1, @@ -121803,6 +122005,22 @@ } } ], + [ + "reformat", + { + "4": { + "113": 1 + } + } + ], + [ + "refocuses", + { + "4": { + "0": 1 + } + } + ], [ "refunded", { @@ -121882,6 +122100,7 @@ "125": 1, "164": 1, "165": 5, + "246": 1, "249": 3, "274": 4 } @@ -121902,7 +122121,10 @@ "198": 1, "236": 1, "246": 1, - "271": 1 + "251": 2, + "260": 1, + "271": 1, + "272": 1 } } ], @@ -121917,6 +122139,7 @@ "29": 1, "163": 1, "193": 2, + "251": 1, "264": 1, "271": 2 } @@ -123170,7 +123393,7 @@ "224": 3, "226": 1, "228": 3, - "229": 4, + "229": 6, "230": 1, "231": 6, "232": 2, @@ -123197,20 +123420,13 @@ "263": 2, "268": 1, "271": 3, + "272": 1, "273": 2, "274": 1, "275": 1 } } ], - [ - "refocuses", - { - "4": { - "0": 1 - } - } - ], [ "rel", { @@ -125355,7 +125571,7 @@ "245": 8, "246": 1, "250": 2, - "251": 1, + "251": 2, "253": 3, "254": 5, "255": 6, @@ -125845,6 +126061,7 @@ "239": 3, "244": 1, "249": 1, + "251": 1, "263": 1, "264": 1, "273": 1 @@ -126583,6 +126800,7 @@ "236": 1, "249": 1, "250": 7, + "251": 1, "257": 1, "270": 1 } @@ -126671,6 +126889,7 @@ "250": 1, "251": 1, "260": 1, + "272": 1, "274": 2, "275": 3 } @@ -127475,6 +127694,7 @@ "237": 2, "239": 1, "242": 2, + "246": 3, "249": 2, "250": 29, "255": 6, @@ -127534,7 +127754,7 @@ "3": { "226": 1, "246": 1, - "272": 2 + "272": 1 }, "4": { "25": 4, @@ -127545,11 +127765,11 @@ "243": 1, "244": 2, "245": 1, - "246": 13, + "246": 14, "248": 13, "250": 1, - "251": 19, - "272": 142 + "251": 25, + "272": 141 } } ], @@ -127819,6 +128039,7 @@ "4": { "8": 1, "112": 1, + "113": 1, "114": 1, "169": 1, "179": 1, @@ -129034,7 +129255,7 @@ "103": 1, "105": 3, "106": 2, - "113": 3, + "113": 5, "114": 6, "120": 1, "121": 1, @@ -129181,7 +129402,7 @@ "108": 4, "110": 3, "112": 1, - "113": 7, + "113": 9, "116": 1, "117": 3, "120": 1, @@ -129638,7 +129859,7 @@ "109": 5, "110": 7, "111": 3, - "112": 7, + "112": 9, "113": 12, "114": 17, "115": 15, @@ -129754,7 +129975,7 @@ "226": 35, "227": 23, "228": 1, - "229": 14, + "229": 15, "230": 20, "231": 22, "232": 12, @@ -129771,7 +129992,7 @@ "243": 8, "244": 24, "245": 5, - "246": 7, + "246": 9, "247": 3, "248": 10, "249": 4, @@ -129996,7 +130217,7 @@ "106": 1, "107": 4, "110": 1, - "112": 2, + "112": 3, "114": 5, "115": 2, "117": 7, @@ -130203,7 +130424,7 @@ "108": 15, "109": 2, "111": 7, - "112": 1, + "112": 3, "113": 1, "114": 40, "115": 12, @@ -130315,6 +130536,7 @@ "243": 2, "244": 3, "245": 2, + "246": 1, "247": 2, "248": 2, "249": 7, @@ -130371,6 +130593,7 @@ "upright", { "4": { + "229": 1, "261": 1 } } @@ -130445,6 +130668,7 @@ "72": 1, "73": 1, "236": 3, + "246": 1, "261": 3, "263": 1 } @@ -131234,6 +131458,7 @@ "241": 4, "242": 3, "244": 1, + "246": 1, "247": 2, "248": 1, "250": 2, @@ -131243,7 +131468,7 @@ "257": 3, "258": 4, "259": 23, - "260": 6, + "260": 7, "261": 2, "262": 3, "263": 1, @@ -131366,7 +131591,7 @@ "108": 1, "109": 1, "112": 6, - "113": 2, + "113": 3, "114": 1, "115": 4, "117": 1, @@ -131454,7 +131679,7 @@ "245": 3, "246": 1, "249": 1, - "251": 1, + "251": 3, "253": 1, "254": 1, "255": 1, @@ -131472,7 +131697,7 @@ "268": 2, "270": 4, "271": 1, - "272": 11, + "272": 12, "273": 1, "274": 1 } @@ -133147,7 +133372,7 @@ "108": 1, "109": 2, "112": 1, - "113": 3, + "113": 4, "117": 1, "118": 2, "119": 1, @@ -133199,7 +133424,7 @@ "223": 1, "225": 3, "226": 1, - "229": 6, + "229": 8, "230": 12, "233": 1, "236": 1, @@ -133211,13 +133436,13 @@ "243": 1, "244": 4, "245": 2, - "246": 1, + "246": 3, "249": 4, "250": 1, "251": 3, "257": 1, "258": 1, - "260": 11, + "260": 12, "271": 1, "274": 6, "275": 3 @@ -133839,7 +134064,7 @@ "143": 1, "144": 1, "158": 2, - "229": 1, + "229": 2, "230": 4 } } @@ -133971,6 +134196,7 @@ "176": 2, "194": 2, "214": 1, + "246": 1, "249": 1, "250": 7, "255": 1, @@ -134316,6 +134542,7 @@ "untranslated", { "4": { + "113": 2, "244": 1 } } @@ -134360,7 +134587,8 @@ "4": { "11": 1, "41": 1, - "232": 1 + "232": 1, + "251": 1 } } ], @@ -135219,7 +135447,7 @@ "242": 1, "243": 1, "244": 1, - "246": 2, + "246": 7, "253": 1, "257": 1, "258": 4, @@ -135298,7 +135526,7 @@ "98": 3, "106": 2, "108": 5, - "113": 32, + "113": 36, "120": 5, "141": 10, "146": 1, @@ -135474,7 +135702,7 @@ "221": 7, "224": 1, "227": 3, - "229": 2, + "229": 3, "230": 1, "232": 2, "233": 2, @@ -135955,7 +136183,7 @@ "235": 4, "240": 6, "244": 2, - "246": 1, + "246": 3, "247": 4, "248": 1, "252": 10, @@ -136292,7 +136520,7 @@ "264": 1, "267": 1, "268": 1, - "272": 8 + "272": 9 }, "4": { "0": 97, @@ -136382,8 +136610,8 @@ "106": 8, "107": 7, "108": 17, - "112": 10, - "113": 23, + "112": 11, + "113": 25, "114": 21, "115": 5, "116": 6, @@ -136482,7 +136710,7 @@ "226": 21, "227": 8, "228": 2, - "229": 97, + "229": 99, "230": 13, "231": 16, "232": 16, @@ -136498,10 +136726,10 @@ "243": 14, "244": 65, "245": 15, - "246": 8, + "246": 9, "247": 23, "248": 3, - "251": 9, + "251": 10, "252": 21, "253": 36, "254": 12, @@ -136510,7 +136738,7 @@ "257": 57, "258": 3, "259": 323, - "260": 124, + "260": 125, "261": 32, "262": 25, "263": 15, @@ -136522,7 +136750,7 @@ "269": 2, "270": 2, "271": 1, - "272": 28, + "272": 31, "273": 1, "274": 1, "275": 3 @@ -136978,6 +137206,14 @@ } } ], + [ + "2853", + { + "4": { + "246": 1 + } + } + ], [ "285654", { @@ -137339,6 +137575,7 @@ "56": 1, "60": 1, "71": 2, + "113": 4, "116": 1, "118": 1, "119": 1, @@ -137398,15 +137635,6 @@ } } ], - [ - "212", - { - "4": { - "147": 2, - "148": 1 - } - } - ], [ "213", { @@ -137418,6 +137646,16 @@ } } ], + [ + "212", + { + "4": { + "113": 1, + "147": 2, + "148": 1 + } + } + ], [ "2160", { @@ -137756,6 +137994,14 @@ } } ], + [ + "2034", + { + "4": { + "246": 1 + } + } + ], [ "203", { @@ -138608,6 +138854,8 @@ "240": 2, "241": 2, "244": 1, + "246": 1, + "251": 1, "255": 2, "256": 2, "257": 1, @@ -138617,7 +138865,7 @@ "263": 5, "267": 4, "268": 8, - "272": 1, + "272": 2, "274": 3 } } @@ -138751,6 +138999,14 @@ } } ], + [ + "2007", + { + "4": { + "246": 1 + } + } + ], [ "200pt", { @@ -138900,6 +139156,7 @@ "237": 1, "244": 2, "248": 6, + "251": 1, "252": 1, "253": 2, "258": 2, @@ -139034,6 +139291,14 @@ } } ], + [ + "2420", + { + "4": { + "246": 2 + } + } + ], [ "2420x1668", { @@ -139246,7 +139511,7 @@ "2622", { "4": { - "246": 1 + "246": 3 } } ], @@ -139435,7 +139700,7 @@ "110": 4, "111": 6, "112": 4, - "113": 14, + "113": 15, "118": 6, "119": 4, "120": 11, @@ -139782,7 +140047,7 @@ "107": 1, "108": 2, "111": 3, - "112": 1, + "112": 4, "114": 1, "118": 6, "119": 4, @@ -139844,7 +140109,7 @@ "225": 1, "226": 1, "228": 1, - "229": 69, + "229": 74, "230": 25, "231": 7, "233": 6, @@ -139856,20 +140121,20 @@ "240": 12, "241": 2, "244": 3, - "246": 15, - "251": 3, + "246": 18, + "251": 7, "252": 1, "255": 2, "256": 5, "257": 8, - "260": 21, + "260": 22, "263": 22, "264": 15, "267": 3, "268": 5, "270": 2, "271": 5, - "272": 1, + "272": 2, "273": 5, "274": 12, "275": 7 @@ -140038,7 +140303,9 @@ { "4": { "232": 1, - "255": 3 + "251": 1, + "255": 3, + "272": 1 } } ], @@ -140553,6 +140820,7 @@ "225": 1, "229": 3, "244": 1, + "246": 1, "260": 3, "264": 2, "271": 2, @@ -140750,7 +141018,7 @@ "223": 3, "226": 5, "227": 2, - "229": 5, + "229": 7, "230": 10, "231": 10, "232": 4, @@ -140763,7 +141031,7 @@ "240": 3, "241": 3, "244": 3, - "246": 7, + "246": 8, "247": 1, "248": 2, "249": 8, @@ -141985,7 +142253,7 @@ "108": 1, "111": 1, "112": 2, - "113": 5, + "113": 6, "114": 2, "117": 1, "122": 1, @@ -142129,7 +142397,7 @@ "106": 1, "108": 2, "112": 1, - "113": 1, + "113": 2, "114": 3, "116": 1, "120": 4, @@ -142762,6 +143030,7 @@ "73": 1, "96": 1, "100": 1, + "113": 1, "115": 1, "118": 2, "119": 2, @@ -142813,7 +143082,7 @@ "261": 1, "263": 1, "267": 1, - "272": 1 + "272": 2 } } ], @@ -142853,7 +143122,8 @@ "flags", { "1": { - "202": 1 + "202": 1, + "251": 1 }, "3": { "7": 1, @@ -143034,7 +143304,7 @@ "246": 3, "249": 8, "250": 7, - "251": 5, + "251": 6, "258": 1, "260": 1, "261": 1, @@ -143129,6 +143399,7 @@ "221": 1, "224": 1, "229": 1, + "246": 1, "254": 9, "266": 1 } @@ -144712,17 +144983,17 @@ "225": 3, "226": 10, "228": 2, - "229": 2, + "229": 3, "231": 4, "232": 17, "234": 8, "241": 6, "242": 1, "244": 4, - "246": 2, + "246": 3, "248": 1, "250": 1, - "251": 1, + "251": 2, "258": 1, "260": 2, "263": 1, @@ -145174,6 +145445,7 @@ "81": 1, "85": 1, "91": 1, + "112": 1, "117": 1, "124": 1, "132": 1, @@ -145909,7 +146181,7 @@ { "4": { "44": 2, - "112": 5, + "112": 6, "113": 4, "176": 1, "179": 1, @@ -145964,6 +146236,7 @@ "82": 1, "85": 1, "95": 1, + "113": 1, "114": 1, "116": 1, "120": 6, @@ -146272,7 +146545,7 @@ "4": { "8": 1, "9": 2, - "112": 1, + "112": 2, "180": 1, "210": 4 } @@ -146724,7 +146997,7 @@ { "4": { "71": 1, - "246": 1 + "246": 2 } } ], @@ -147172,8 +147445,10 @@ "238": 1, "239": 31, "240": 3, + "246": 1, "249": 5, "250": 6, + "251": 4, "252": 3, "253": 4, "255": 13, @@ -147187,7 +147462,7 @@ "263": 11, "266": 3, "270": 1, - "272": 3 + "272": 6 } } ], @@ -147735,7 +148010,7 @@ "103": 5, "108": 1, "112": 1, - "113": 5, + "113": 6, "114": 1, "119": 2, "133": 1, @@ -147999,6 +148274,7 @@ "93": 1, "107": 1, "108": 2, + "112": 1, "113": 1, "115": 1, "119": 1, @@ -148066,7 +148342,7 @@ "219": 1, "221": 1, "223": 1, - "229": 2, + "229": 3, "230": 4, "231": 1, "235": 1, @@ -148096,7 +148372,7 @@ "45": 1, "163": 1, "218": 1, - "229": 3 + "229": 4 } } ], @@ -148106,7 +148382,7 @@ "4": { "45": 3, "173": 1, - "229": 9, + "229": 10, "235": 1 } } @@ -148367,6 +148643,7 @@ "230": 1, "231": 1, "242": 1, + "260": 1, "268": 1 } } @@ -148813,7 +149090,7 @@ "222": 1, "226": 2, "227": 2, - "229": 2, + "229": 4, "232": 4, "237": 1, "240": 2, @@ -149379,7 +149656,7 @@ "107": 1, "108": 1, "109": 1, - "112": 3, + "112": 4, "113": 1, "118": 1, "119": 3, @@ -149999,6 +150276,7 @@ "237": 1, "242": 4, "250": 1, + "251": 5, "254": 5, "256": 1, "258": 10, @@ -150197,7 +150475,8 @@ "109": 1, "110": 3, "111": 1, - "112": 5, + "112": 6, + "113": 2, "114": 5, "115": 6, "117": 4, @@ -150331,7 +150610,7 @@ "268": 2, "270": 2, "271": 8, - "272": 4, + "272": 5, "274": 5, "275": 6 } @@ -150662,6 +150941,7 @@ "33": 1, "42": 2, "51": 6, + "113": 2, "116": 1, "157": 1, "179": 2, @@ -150734,7 +151014,7 @@ "98": 3, "103": 3, "107": 2, - "112": 1, + "112": 2, "113": 10, "114": 1, "118": 1, @@ -150924,7 +151204,7 @@ "107": 3, "108": 8, "112": 1, - "113": 12, + "113": 17, "114": 13, "115": 7, "116": 2, @@ -151269,6 +151549,7 @@ "102": 3, "103": 1, "112": 2, + "113": 1, "120": 3, "127": 3, "134": 1, @@ -151820,8 +152101,8 @@ "107": 1, "108": 1, "109": 1, - "112": 3, - "113": 1, + "112": 4, + "113": 3, "115": 2, "120": 1, "129": 1, @@ -152388,6 +152669,7 @@ "240": 3, "241": 2, "244": 2, + "251": 1, "253": 5, "254": 2, "255": 1, @@ -152396,6 +152678,7 @@ "260": 3, "261": 1, "270": 1, + "272": 1, "273": 1, "274": 2, "275": 1 @@ -152570,7 +152853,7 @@ "247": 1, "248": 1, "250": 3, - "251": 1, + "251": 2, "252": 1, "253": 3, "254": 10, @@ -152965,6 +153248,7 @@ "50": 3, "51": 8, "68": 2, + "113": 2, "115": 1, "117": 4, "142": 2, @@ -153113,6 +153397,7 @@ "71": 1, "72": 1, "82": 1, + "113": 1, "115": 1, "119": 1, "122": 1, @@ -153224,7 +153509,7 @@ "109": 1, "110": 1, "111": 1, - "113": 5, + "113": 13, "114": 1, "115": 3, "118": 3, @@ -153300,7 +153585,7 @@ "226": 5, "227": 5, "228": 3, - "229": 8, + "229": 10, "230": 5, "231": 5, "232": 1, @@ -153311,7 +153596,7 @@ "241": 4, "242": 1, "245": 1, - "246": 2, + "246": 4, "249": 4, "250": 1, "251": 4, @@ -153452,6 +153737,7 @@ "101": 1, "120": 1, "156": 1, + "229": 1, "251": 1, "253": 1, "256": 1 @@ -155076,6 +155362,7 @@ "239": 1, "242": 3, "244": 3, + "246": 1, "254": 3, "259": 1, "269": 1, @@ -155388,7 +155675,7 @@ "226": 12, "227": 3, "228": 1, - "229": 4, + "229": 5, "230": 1, "232": 21, "234": 17, @@ -155874,7 +156161,7 @@ "106": 8, "107": 1, "109": 2, - "113": 42, + "113": 43, "114": 9, "115": 2, "116": 24, @@ -155919,7 +156206,7 @@ "223": 9, "224": 3, "226": 15, - "229": 7, + "229": 9, "230": 9, "232": 1, "233": 10, @@ -156273,7 +156560,7 @@ "246": 1, "249": 1, "250": 3, - "251": 1, + "251": 3, "252": 1, "253": 8, "255": 1, @@ -156289,6 +156576,7 @@ "268": 1, "270": 3, "271": 4, + "272": 2, "273": 4, "274": 1, "275": 4 @@ -157298,6 +157586,7 @@ "108": 3, "110": 1, "112": 4, + "113": 1, "115": 6, "117": 5, "119": 1, @@ -157364,9 +157653,9 @@ "240": 1, "243": 1, "244": 3, - "246": 1, + "246": 2, "249": 1, - "251": 1, + "251": 2, "253": 1, "255": 1, "256": 2, @@ -157816,6 +158105,7 @@ "221": 2, "223": 2, "226": 2, + "229": 1, "230": 2, "233": 2, "236": 2, @@ -158050,6 +158340,7 @@ "106": 4, "107": 1, "111": 3, + "112": 1, "113": 1, "115": 1, "117": 1, @@ -158301,7 +158592,7 @@ "216": 1, "243": 1, "248": 1, - "272": 2 + "272": 3 }, "4": { "0": 4, @@ -158375,7 +158666,7 @@ "108": 5, "111": 1, "112": 1, - "113": 1, + "113": 3, "114": 1, "115": 4, "116": 1, @@ -158480,7 +158771,7 @@ "269": 1, "270": 2, "271": 6, - "272": 4, + "272": 5, "274": 5, "275": 2 } @@ -158657,8 +158948,8 @@ "107": 2, "110": 1, "111": 1, - "112": 3, - "113": 1, + "112": 4, + "113": 2, "114": 1, "115": 2, "117": 1, @@ -158731,7 +159022,7 @@ "225": 1, "226": 1, "228": 1, - "229": 3, + "229": 6, "230": 2, "231": 1, "233": 3, @@ -158763,6 +159054,7 @@ "269": 2, "270": 2, "271": 2, + "272": 1, "273": 3, "274": 3, "275": 1 @@ -158928,7 +159220,7 @@ "226": 1, "227": 1, "228": 1, - "229": 4, + "229": 5, "230": 4, "233": 2, "234": 1, @@ -160630,6 +160922,7 @@ "106": 1, "107": 1, "108": 3, + "113": 3, "114": 77, "116": 2, "119": 1, @@ -160963,7 +161256,7 @@ }, "4": { "112": 10, - "113": 4 + "113": 6 } } ], @@ -160974,8 +161267,8 @@ "112": 1 }, "4": { - "112": 11, - "113": 5, + "112": 12, + "113": 7, "273": 3 } } @@ -161021,7 +161314,7 @@ "0": 1, "64": 1, "112": 6, - "113": 16, + "113": 17, "235": 1, "260": 1, "273": 3 @@ -161061,7 +161354,7 @@ "77": 1, "79": 1, "112": 25, - "113": 12, + "113": 16, "179": 1, "215": 1, "227": 26, @@ -161178,6 +161471,7 @@ "210": 1, "226": 1, "237": 1, + "246": 2, "255": 1, "259": 1 } @@ -161382,6 +161676,7 @@ "222": 29, "224": 2, "226": 1, + "229": 1, "231": 2, "233": 16, "239": 3, @@ -161420,6 +161715,7 @@ "187": 1, "189": 2, "194": 4, + "229": 1, "235": 1 } } @@ -161929,7 +162225,7 @@ "106": 1, "109": 6, "112": 2, - "113": 1, + "113": 2, "130": 1, "139": 4, "140": 3, @@ -161941,7 +162237,7 @@ "221": 5, "222": 1, "224": 6, - "229": 16, + "229": 21, "230": 3, "233": 2, "235": 1, @@ -162016,7 +162312,7 @@ "183": 10, "184": 6, "221": 1, - "229": 2, + "229": 3, "230": 1, "231": 1, "236": 17, @@ -162640,6 +162936,7 @@ "94": 1, "100": 2, "112": 2, + "113": 1, "115": 1, "124": 1, "147": 41, @@ -163340,6 +163637,7 @@ "67": 14, "82": 2, "92": 1, + "113": 1, "115": 1, "119": 1, "122": 1, @@ -163365,20 +163663,21 @@ "221": 1, "223": 3, "227": 3, + "229": 1, "237": 1, "240": 2, "242": 1, "244": 1, "246": 2, "247": 1, - "251": 4, + "251": 9, "255": 2, "256": 10, "257": 1, "260": 5, "266": 1, "271": 2, - "272": 11 + "272": 12 } } ], @@ -164018,7 +164317,7 @@ "240": 1, "244": 1, "246": 1, - "251": 3, + "251": 5, "255": 1 } } @@ -164077,7 +164376,7 @@ "225": 4, "226": 16, "228": 2, - "229": 2, + "229": 5, "230": 3, "231": 1, "236": 2, @@ -164086,7 +164385,7 @@ "240": 2, "244": 1, "246": 3, - "251": 5, + "251": 6, "256": 1, "263": 1, "264": 1, @@ -164382,7 +164681,7 @@ "91": 1, "93": 2, "106": 2, - "113": 4, + "113": 8, "116": 3, "120": 1, "127": 2, @@ -164427,7 +164726,7 @@ "224": 7, "226": 1, "228": 1, - "229": 9, + "229": 12, "230": 11, "232": 3, "233": 2, @@ -164435,7 +164734,7 @@ "237": 2, "238": 1, "240": 44, - "246": 1, + "246": 2, "249": 1, "250": 6, "251": 1, @@ -164641,7 +164940,7 @@ "186": 1, "215": 3, "221": 65, - "229": 23, + "229": 30, "230": 1, "231": 1, "233": 13, @@ -164782,13 +165081,13 @@ "218": 2, "226": 1, "228": 5, - "229": 1, + "229": 3, "230": 1, "236": 3, "239": 1, "241": 1, "244": 5, - "246": 5, + "246": 7, "247": 1, "249": 4, "250": 2, @@ -164801,6 +165100,7 @@ "263": 6, "268": 3, "271": 3, + "272": 1, "273": 12, "274": 32, "275": 14 @@ -164915,7 +165215,7 @@ "98": 1, "103": 32, "112": 4, - "113": 1, + "113": 7, "127": 1, "132": 5, "146": 1, @@ -164948,7 +165248,7 @@ "231": 4, "237": 2, "244": 4, - "246": 8, + "246": 9, "247": 2, "248": 1, "249": 3, @@ -165087,7 +165387,7 @@ "225": 8, "226": 1, "227": 1, - "229": 1, + "229": 2, "230": 1, "231": 3, "237": 4, @@ -165096,6 +165396,8 @@ "243": 1, "244": 3, "245": 2, + "246": 1, + "251": 1, "255": 1, "257": 2, "260": 2, @@ -165157,6 +165459,7 @@ "232": 1, "239": 3, "247": 1, + "251": 1, "253": 1, "257": 3, "259": 1, @@ -165432,6 +165735,7 @@ "248": 3, "249": 7, "250": 3, + "251": 2, "252": 1, "254": 2, "257": 1, @@ -165448,7 +165752,7 @@ "269": 2, "270": 2, "271": 1, - "272": 3, + "272": 4, "273": 47, "274": 3, "275": 5 @@ -165760,6 +166064,7 @@ "209": 1, "210": 1, "216": 1, + "229": 1, "230": 1 } } @@ -165816,7 +166121,7 @@ "107": 2, "108": 3, "110": 1, - "113": 1, + "113": 2, "117": 2, "118": 2, "119": 1, @@ -165869,8 +166174,9 @@ "240": 2, "241": 1, "244": 3, + "246": 1, "250": 2, - "251": 1, + "251": 2, "254": 1, "255": 1, "257": 3, @@ -166672,7 +166978,7 @@ "103": 3, "106": 1, "112": 1, - "113": 24, + "113": 26, "115": 1, "121": 1, "130": 1, @@ -166793,10 +167099,11 @@ "243": 2, "244": 5, "247": 2, + "251": 2, "260": 6, "266": 19, "267": 3, - "272": 1, + "272": 2, "273": 3 } } @@ -166877,7 +167184,8 @@ "261": 1, "262": 6, "263": 2, - "265": 2 + "265": 2, + "272": 1 } } ], @@ -166967,12 +167275,14 @@ "204": 4, "210": 1, "223": 1, + "229": 1, "233": 1, "234": 1, "238": 2, "239": 1, "243": 1, "244": 3, + "251": 1, "254": 4, "255": 1, "262": 1, @@ -166993,7 +167303,7 @@ "89": 2, "102": 1, "175": 1, - "251": 1, + "251": 3, "263": 1 }, "3": { @@ -167077,6 +167387,7 @@ "225": 2, "226": 3, "227": 37, + "229": 6, "230": 1, "232": 12, "233": 1, @@ -167088,7 +167399,7 @@ "244": 35, "245": 17, "246": 1, - "251": 11, + "251": 37, "255": 6, "256": 7, "257": 4, @@ -167100,7 +167411,7 @@ "266": 3, "270": 1, "271": 1, - "272": 40, + "272": 71, "273": 2 } } @@ -167643,8 +167954,8 @@ "107": 1, "108": 1, "109": 4, - "112": 4, - "113": 42, + "112": 6, + "113": 43, "115": 6, "116": 4, "117": 10, @@ -168130,6 +168441,7 @@ "186": 1, "221": 1, "223": 1, + "229": 1, "262": 1 } } @@ -168254,6 +168566,7 @@ "210": 1, "216": 1, "224": 1, + "229": 1, "234": 2, "236": 1, "237": 1, @@ -169703,7 +170016,7 @@ "226": 10, "227": 5, "228": 7, - "229": 3, + "229": 4, "230": 2, "231": 2, "232": 20, @@ -169718,7 +170031,7 @@ "243": 37, "244": 135, "245": 42, - "246": 7, + "246": 9, "247": 2, "250": 6, "251": 5, @@ -169995,6 +170308,7 @@ "241": 1, "246": 2, "249": 1, + "251": 1, "254": 2, "257": 3, "258": 4, @@ -170228,7 +170542,7 @@ "107": 2, "108": 1, "110": 1, - "113": 15, + "113": 16, "114": 6, "115": 3, "118": 1, @@ -170309,7 +170623,7 @@ "226": 3, "227": 2, "228": 1, - "229": 1, + "229": 2, "231": 1, "232": 1, "233": 2, @@ -170321,6 +170635,7 @@ "246": 3, "248": 1, "250": 4, + "251": 1, "252": 1, "254": 1, "255": 2, @@ -170671,6 +170986,7 @@ "overflowed", { "4": { + "229": 2, "240": 1 } } @@ -170694,7 +171010,7 @@ "166": 1, "174": 1, "198": 1, - "229": 13, + "229": 15, "240": 24, "250": 2 } @@ -170706,7 +171022,7 @@ "4": { "0": 1, "199": 1, - "229": 1, + "229": 2, "240": 1 } } @@ -171157,6 +171473,7 @@ "245": 2, "246": 14, "250": 3, + "251": 1, "252": 1, "255": 2, "256": 3, @@ -171263,7 +171580,7 @@ "108": 1, "111": 3, "112": 18, - "113": 3, + "113": 6, "116": 1, "118": 3, "119": 2, @@ -171338,7 +171655,7 @@ "225": 2, "226": 1, "227": 10, - "229": 6, + "229": 7, "230": 8, "231": 3, "232": 4, @@ -171704,7 +172021,7 @@ "219": 1, "223": 2, "230": 1, - "246": 3, + "246": 2, "259": 1, "261": 1, "266": 1, @@ -171730,7 +172047,7 @@ "169": 1, "214": 3, "221": 1, - "246": 1, + "246": 2, "248": 1, "250": 1, "251": 2, @@ -172565,7 +172882,7 @@ "223": 2, "224": 2, "228": 1, - "229": 7, + "229": 10, "230": 1, "231": 1, "232": 3, @@ -172594,7 +172911,7 @@ "268": 1, "270": 4, "271": 1, - "272": 1, + "272": 3, "274": 2 } } @@ -173722,7 +174039,7 @@ "224": 4, "226": 2, "227": 2, - "229": 2, + "229": 5, "232": 14, "233": 4, "234": 4, @@ -173938,11 +174255,12 @@ "188": 6, "218": 2, "220": 1, - "229": 22, + "229": 23, "230": 2, "231": 5, "235": 1, "240": 1, + "246": 6, "254": 2, "265": 1 } @@ -173985,6 +174303,7 @@ "28": 1, "31": 1, "44": 1, + "113": 1, "152": 1, "157": 2, "181": 1, @@ -174318,6 +174637,8 @@ "106": 2, "109": 2, "110": 1, + "112": 1, + "113": 1, "114": 1, "115": 2, "117": 3, @@ -174405,7 +174726,7 @@ "254": 1, "257": 5, "259": 1, - "260": 2, + "260": 3, "261": 1, "262": 2, "263": 3, @@ -174659,8 +174980,8 @@ "106": 1, "107": 1, "111": 1, - "112": 2, - "113": 4, + "112": 4, + "113": 7, "114": 5, "118": 4, "119": 11, @@ -174741,7 +175062,7 @@ "225": 1, "226": 7, "228": 1, - "229": 9, + "229": 10, "230": 10, "231": 5, "232": 6, @@ -174753,7 +175074,7 @@ "241": 1, "244": 2, "245": 1, - "246": 1, + "246": 2, "248": 1, "251": 2, "252": 1, @@ -174927,6 +175248,7 @@ "224": 1, "225": 3, "226": 12, + "229": 2, "230": 1, "233": 1, "240": 1, @@ -174968,6 +175290,7 @@ "217": 1, "221": 1, "227": 1, + "229": 1, "255": 1 } } @@ -175310,6 +175633,9 @@ [ "opened", { + "3": { + "272": 1 + }, "4": { "61": 1, "64": 1, @@ -175319,6 +175645,7 @@ "146": 1, "156": 1, "215": 1, + "272": 1, "273": 1, "275": 1 } @@ -175396,11 +175723,12 @@ "221": 2, "222": 1, "226": 1, - "229": 2, + "229": 3, "232": 1, "234": 1, "238": 1, "240": 1, + "251": 1, "255": 1, "256": 1, "263": 2, @@ -175552,7 +175880,7 @@ "224": 5, "225": 4, "226": 4, - "229": 23, + "229": 28, "230": 1, "231": 1, "232": 6, @@ -175564,9 +175892,9 @@ "239": 1, "243": 2, "244": 7, - "246": 3, + "246": 5, "249": 4, - "251": 1, + "251": 2, "253": 3, "256": 1, "260": 3, @@ -175967,6 +176295,7 @@ "243": 2, "245": 1, "248": 1, + "251": 1, "253": 2, "254": 1, "255": 2, @@ -176309,6 +176638,7 @@ "244": 3, "247": 1, "250": 1, + "251": 1, "253": 2, "255": 1, "257": 6, @@ -176370,7 +176700,7 @@ "196": 2, "211": 1, "217": 1, - "229": 1, + "229": 2, "230": 1, "235": 1, "236": 1, @@ -176695,7 +177025,7 @@ "229": 3, "231": 2, "244": 1, - "260": 4 + "260": 5 } } ], @@ -176723,16 +177053,16 @@ "158": 2, "170": 1, "210": 2, - "229": 3, + "229": 5, "230": 3, "231": 8, "236": 7, "237": 1, "244": 5, - "246": 2, + "246": 4, "251": 2, "253": 2, - "260": 12, + "260": 15, "261": 39, "263": 3, "266": 1 @@ -176996,6 +177326,7 @@ "92": 1, "93": 4, "103": 20, + "113": 2, "117": 2, "146": 2, "152": 1, @@ -177127,7 +177458,7 @@ "108": 1, "109": 3, "110": 2, - "113": 2, + "113": 8, "116": 2, "117": 4, "118": 3, @@ -177201,7 +177532,7 @@ "265": 7, "267": 2, "270": 3, - "272": 1 + "272": 2 } } ], @@ -177448,7 +177779,7 @@ "226": 7, "227": 1, "228": 2, - "229": 1, + "229": 2, "230": 1, "232": 6, "233": 2, @@ -177708,7 +178039,7 @@ "110": 3, "111": 7, "112": 5, - "113": 5, + "113": 9, "114": 9, "115": 9, "116": 3, @@ -177820,7 +178151,7 @@ "226": 15, "227": 9, "228": 3, - "229": 22, + "229": 26, "230": 21, "231": 12, "232": 6, @@ -177837,12 +178168,12 @@ "243": 2, "244": 18, "245": 1, - "246": 29, + "246": 32, "247": 1, "248": 13, "249": 7, "250": 5, - "251": 10, + "251": 11, "252": 1, "253": 1, "254": 8, @@ -177851,7 +178182,7 @@ "257": 14, "258": 10, "259": 2, - "260": 18, + "260": 19, "261": 5, "262": 12, "263": 21, @@ -178087,7 +178418,7 @@ "215": 1, "225": 1, "241": 1, - "272": 1 + "272": 2 }, "4": { "0": 9, @@ -178195,7 +178526,7 @@ "110": 3, "111": 2, "112": 2, - "113": 21, + "113": 29, "114": 1, "115": 3, "116": 1, @@ -178299,7 +178630,7 @@ "226": 9, "227": 4, "228": 3, - "229": 14, + "229": 18, "230": 14, "231": 6, "232": 5, @@ -178312,12 +178643,12 @@ "241": 7, "242": 1, "244": 6, - "246": 14, + "246": 15, "247": 1, "248": 1, "249": 3, "250": 8, - "251": 9, + "251": 16, "252": 5, "253": 3, "254": 13, @@ -178326,7 +178657,7 @@ "257": 8, "258": 6, "259": 14, - "260": 6, + "260": 7, "261": 1, "262": 8, "263": 6, @@ -178338,7 +178669,7 @@ "269": 5, "270": 6, "271": 10, - "272": 1, + "272": 6, "273": 7, "274": 9, "275": 11 @@ -178887,8 +179218,8 @@ "109": 12, "110": 18, "111": 22, - "112": 3, - "113": 11, + "112": 5, + "113": 12, "114": 15, "115": 22, "116": 31, @@ -179016,12 +179347,12 @@ "243": 1, "244": 28, "245": 4, - "246": 3, + "246": 6, "247": 18, "248": 19, "249": 12, "250": 8, - "251": 3, + "251": 5, "252": 6, "253": 1, "254": 6, @@ -179029,7 +179360,7 @@ "256": 31, "257": 44, "259": 34, - "260": 56, + "260": 58, "261": 15, "262": 44, "263": 103, @@ -180279,7 +180610,7 @@ "226": 5, "227": 2, "228": 1, - "229": 6, + "229": 11, "230": 1, "232": 3, "235": 2, @@ -181550,7 +181881,8 @@ "174": 1, "196": 2, "214": 1, - "263": 1 + "263": 1, + "272": 1 }, "4": { "0": 4, @@ -181628,7 +181960,7 @@ "109": 2, "110": 4, "111": 2, - "113": 5, + "113": 6, "114": 4, "115": 10, "117": 4, @@ -181711,7 +182043,7 @@ "226": 9, "227": 1, "228": 4, - "229": 18, + "229": 21, "230": 6, "232": 5, "233": 1, @@ -181725,7 +182057,7 @@ "246": 4, "249": 4, "250": 1, - "251": 4, + "251": 5, "252": 2, "253": 3, "254": 6, @@ -181744,7 +182076,7 @@ "269": 1, "270": 11, "271": 8, - "272": 1, + "272": 2, "273": 1, "274": 2 } @@ -182131,6 +182463,7 @@ "224": 1, "230": 1, "233": 1, + "246": 1, "252": 1, "261": 3, "262": 3, @@ -182841,7 +183174,7 @@ "106": 4, "107": 1, "110": 1, - "112": 10, + "112": 12, "113": 1, "114": 6, "115": 5, @@ -182967,6 +183300,7 @@ "268": 6, "270": 1, "271": 4, + "272": 1, "273": 1, "274": 2, "275": 3 @@ -183115,7 +183449,7 @@ "224": 2, "226": 2, "228": 3, - "229": 2, + "229": 3, "230": 1, "232": 1, "234": 4, @@ -183126,7 +183460,7 @@ "240": 2, "241": 1, "244": 7, - "246": 6, + "246": 8, "249": 1, "251": 2, "253": 1, @@ -183339,6 +183673,22 @@ } } ], + [ + "newmeta", + { + "4": { + "162": 1 + } + } + ], + [ + "newvideo", + { + "4": { + "162": 1 + } + } + ], [ "newlines", { @@ -183347,6 +183697,14 @@ } } ], + [ + "newline", + { + "4": { + "113": 1 + } + } + ], [ "newly", { @@ -183362,22 +183720,6 @@ } } ], - [ - "newmeta", - { - "4": { - "162": 1 - } - } - ], - [ - "newvideo", - { - "4": { - "162": 1 - } - } - ], [ "newspapers", { @@ -183589,7 +183931,7 @@ "109": 2, "110": 1, "112": 3, - "113": 12, + "113": 13, "114": 2, "115": 4, "116": 2, @@ -183700,7 +184042,7 @@ "241": 6, "243": 1, "244": 7, - "246": 1, + "246": 3, "247": 5, "248": 2, "249": 1, @@ -184544,6 +184886,7 @@ "103": 1, "104": 3, "105": 1, + "113": 2, "118": 6, "119": 1, "124": 1, @@ -184582,7 +184925,7 @@ "221": 1, "223": 3, "226": 1, - "229": 2, + "229": 3, "230": 3, "235": 3, "237": 2, @@ -184590,6 +184933,7 @@ "246": 3, "248": 3, "249": 1, + "251": 1, "254": 1, "257": 1, "263": 1, @@ -184766,7 +185110,7 @@ "226": 4, "227": 2, "228": 2, - "229": 3, + "229": 4, "230": 1, "231": 2, "232": 1, @@ -184776,7 +185120,7 @@ "238": 1, "241": 1, "244": 2, - "246": 1, + "246": 2, "249": 4, "250": 5, "255": 2, @@ -185163,8 +185507,8 @@ "109": 2, "110": 7, "111": 7, - "112": 6, - "113": 7, + "112": 7, + "113": 10, "114": 15, "115": 28, "116": 10, @@ -185278,7 +185622,7 @@ "226": 19, "227": 9, "228": 9, - "229": 11, + "229": 16, "230": 14, "231": 10, "232": 37, @@ -185295,12 +185639,12 @@ "243": 1, "244": 30, "245": 1, - "246": 29, + "246": 33, "247": 1, "248": 16, "249": 12, "250": 28, - "251": 16, + "251": 33, "252": 1, "253": 4, "254": 20, @@ -185321,7 +185665,7 @@ "269": 1, "270": 8, "271": 13, - "272": 5, + "272": 6, "273": 7, "274": 16, "275": 12 @@ -185480,6 +185824,7 @@ "60": 1, "72": 1, "79": 3, + "113": 1, "118": 4, "123": 1, "128": 1, @@ -185901,7 +186246,7 @@ "248": 2, "249": 1, "250": 1, - "251": 3, + "251": 2, "253": 2, "258": 1, "259": 1, @@ -186088,7 +186433,7 @@ "110": 1, "111": 2, "112": 1, - "113": 2, + "113": 4, "115": 1, "116": 2, "117": 1, @@ -186140,6 +186485,7 @@ "225": 2, "226": 3, "228": 1, + "229": 2, "230": 5, "231": 1, "232": 11, @@ -186150,9 +186496,9 @@ "238": 2, "239": 3, "240": 2, - "246": 2, + "246": 5, "250": 1, - "251": 2, + "251": 4, "256": 2, "257": 1, "260": 1, @@ -186164,6 +186510,7 @@ "268": 1, "270": 2, "271": 5, + "272": 2, "274": 2 } } @@ -186368,8 +186715,8 @@ "109": 3, "110": 4, "111": 4, - "112": 10, - "113": 29, + "112": 11, + "113": 38, "114": 18, "115": 51, "116": 9, @@ -186485,7 +186832,7 @@ "226": 37, "227": 10, "228": 13, - "229": 6, + "229": 9, "230": 25, "231": 16, "232": 49, @@ -186502,7 +186849,7 @@ "243": 1, "244": 39, "245": 1, - "246": 33, + "246": 36, "247": 3, "248": 8, "249": 14, @@ -186516,7 +186863,7 @@ "257": 9, "258": 19, "259": 14, - "260": 17, + "260": 18, "261": 35, "262": 11, "263": 22, @@ -186528,7 +186875,7 @@ "269": 7, "270": 18, "271": 31, - "272": 5, + "272": 6, "273": 9, "274": 17, "275": 13 @@ -187117,13 +187464,14 @@ "240": 1, "241": 1, "244": 1, - "246": 2, + "246": 3, "247": 2, "249": 2, "250": 9, "251": 1, "257": 2, "271": 1, + "272": 1, "273": 5, "274": 1, "275": 3 @@ -187238,6 +187586,7 @@ "238": 3, "241": 2, "244": 1, + "246": 1, "253": 1, "257": 1, "263": 1, @@ -187264,7 +187613,8 @@ "186": 2, "192": 2, "196": 5, - "248": 1 + "248": 1, + "272": 1 }, "4": { "0": 12, @@ -187357,8 +187707,8 @@ "107": 12, "108": 5, "109": 5, - "112": 1, - "113": 20, + "112": 2, + "113": 21, "114": 8, "115": 3, "116": 6, @@ -187450,7 +187800,7 @@ "225": 9, "226": 18, "227": 3, - "229": 6, + "229": 8, "230": 5, "231": 3, "232": 9, @@ -187471,7 +187821,7 @@ "248": 5, "249": 4, "250": 11, - "251": 1, + "251": 2, "253": 3, "254": 8, "255": 1, @@ -187487,6 +187837,7 @@ "269": 2, "270": 2, "271": 2, + "272": 1, "273": 60, "275": 6 } @@ -187885,7 +188236,7 @@ "226": 2, "227": 4, "228": 2, - "229": 28, + "229": 35, "230": 5, "231": 7, "232": 207, @@ -187899,6 +188250,7 @@ "244": 5, "246": 1, "247": 12, + "251": 1, "253": 1, "257": 2, "259": 27, @@ -187920,6 +188272,14 @@ } } ], + [ + "lcd", + { + "4": { + "246": 3 + } + } + ], [ "lru", { @@ -188023,6 +188383,7 @@ "lproj", { "4": { + "112": 3, "113": 1, "179": 7, "180": 9 @@ -188258,6 +188619,7 @@ "103": 1, "104": 2, "107": 1, + "112": 1, "135": 7, "151": 2, "156": 3, @@ -188545,6 +188907,7 @@ "4": { "12": 1, "13": 1, + "112": 1, "153": 20 } } @@ -188768,7 +189131,7 @@ "106": 4, "108": 7, "111": 3, - "112": 3, + "112": 4, "115": 1, "118": 2, "119": 2, @@ -189255,6 +189618,7 @@ "244": 3, "247": 1, "250": 1, + "251": 1, "257": 1, "258": 2, "259": 7, @@ -190623,7 +190987,7 @@ "107": 1, "111": 1, "112": 12, - "113": 52, + "113": 59, "115": 1, "116": 1, "117": 1, @@ -190668,7 +191032,8 @@ "localizable", { "4": { - "113": 19, + "112": 1, + "113": 24, "210": 5, "218": 1, "244": 1 @@ -190697,7 +191062,7 @@ "58": 2, "92": 1, "112": 16, - "113": 29, + "113": 30, "116": 3, "169": 1, "171": 1, @@ -190721,7 +191086,7 @@ { "4": { "57": 1, - "113": 10, + "113": 12, "175": 1, "210": 1, "273": 2, @@ -190764,7 +191129,7 @@ "14": 2, "17": 1, "35": 1, - "112": 7, + "112": 8, "113": 1 } } @@ -191412,7 +191777,6 @@ "166": 1, "171": 1, "182": 1, - "251": 1, "253": 1, "263": 1 } @@ -191685,6 +192049,14 @@ } } ], + [ + "lire", + { + "4": { + "112": 1 + } + } + ], [ "li", { @@ -192901,7 +193273,8 @@ { "4": { "74": 1, - "75": 1 + "75": 1, + "229": 1 } } ], @@ -193011,7 +193384,7 @@ "79": 1, "80": 1, "96": 1, - "113": 1, + "113": 2, "155": 1, "164": 2, "165": 1, @@ -193515,6 +193888,7 @@ "241": 2, "249": 1, "250": 1, + "251": 2, "257": 4, "262": 3, "263": 1, @@ -193541,7 +193915,7 @@ "196": 7, "216": 3, "244": 1, - "272": 1 + "272": 2 }, "4": { "0": 1, @@ -193593,6 +193967,7 @@ "105": 4, "106": 2, "111": 1, + "112": 1, "113": 1, "114": 3, "115": 1, @@ -193679,7 +194054,7 @@ "246": 6, "248": 13, "250": 14, - "251": 1, + "251": 2, "253": 1, "257": 8, "258": 1, @@ -193693,7 +194068,7 @@ "268": 2, "270": 2, "271": 10, - "272": 5, + "272": 7, "273": 34, "274": 19, "275": 28 @@ -193960,6 +194335,7 @@ "241": 1, "244": 6, "247": 2, + "251": 1, "255": 1, "258": 10, "262": 3, @@ -194156,6 +194532,7 @@ "106": 1, "108": 1, "112": 1, + "113": 3, "115": 1, "116": 1, "118": 2, @@ -194217,6 +194594,7 @@ "248": 3, "249": 1, "250": 3, + "251": 1, "252": 2, "254": 1, "257": 20, @@ -194382,6 +194760,7 @@ "103": 1, "104": 2, "106": 5, + "113": 1, "114": 1, "116": 6, "117": 9, @@ -194630,7 +195009,7 @@ "106": 5, "108": 1, "112": 1, - "113": 1, + "113": 9, "115": 9, "116": 4, "117": 10, @@ -195560,7 +195939,7 @@ "230": 1, "231": 1, "239": 1, - "246": 1, + "246": 2, "251": 1, "257": 1, "274": 1 @@ -195595,6 +195974,7 @@ "223": 1, "232": 1, "239": 1, + "251": 1, "256": 1, "259": 1, "266": 1, @@ -195737,7 +196117,7 @@ "223": 3, "224": 7, "226": 1, - "229": 11, + "229": 12, "230": 8, "231": 2, "234": 2, @@ -196222,7 +196602,7 @@ "71": 2, "72": 5, "112": 3, - "113": 8, + "113": 9, "118": 1, "119": 1, "128": 1, @@ -196245,7 +196625,7 @@ "224": 2, "226": 1, "227": 4, - "229": 14, + "229": 17, "230": 1, "231": 2, "232": 1, @@ -196253,6 +196633,7 @@ "237": 2, "240": 3, "244": 6, + "246": 4, "253": 1, "257": 4, "261": 26, @@ -196404,7 +196785,7 @@ "224": 4, "227": 1, "228": 2, - "229": 1, + "229": 2, "230": 3, "231": 3, "232": 4, @@ -197368,7 +197749,7 @@ "107": 4, "109": 1, "112": 5, - "113": 6, + "113": 7, "121": 2, "122": 2, "126": 1, @@ -197426,7 +197807,7 @@ "224": 2, "226": 16, "227": 1, - "229": 1, + "229": 2, "230": 3, "233": 6, "234": 1, @@ -197438,7 +197819,7 @@ "244": 9, "245": 5, "246": 2, - "251": 3, + "251": 5, "253": 22, "255": 1, "257": 2, @@ -197452,7 +197833,7 @@ "267": 7, "269": 1, "270": 11, - "272": 13, + "272": 14, "273": 1, "274": 10 } @@ -197551,7 +197932,8 @@ "4": { "76": 1, "229": 2, - "240": 1 + "240": 1, + "260": 1 } } ], @@ -197716,6 +198098,7 @@ "225": 1, "226": 1, "228": 1, + "229": 1, "234": 3, "237": 2, "239": 1, @@ -197852,7 +198235,7 @@ "109": 1, "110": 1, "112": 13, - "113": 4, + "113": 8, "117": 1, "130": 10, "133": 1, @@ -197881,7 +198264,7 @@ "226": 10, "227": 1, "228": 1, - "229": 27, + "229": 28, "230": 88, "231": 56, "232": 7, @@ -198048,7 +198431,7 @@ "103": 4, "104": 1, "112": 4, - "113": 15, + "113": 17, "118": 1, "127": 2, "128": 3, @@ -198130,7 +198513,8 @@ "98": 1, "142": 1, "182": 1, - "191": 1 + "191": 1, + "251": 1 } } ], @@ -198194,7 +198578,7 @@ "147": 1, "210": 3, "221": 1, - "229": 7, + "229": 9, "230": 2, "231": 8, "233": 1, @@ -198203,8 +198587,9 @@ "240": 1, "243": 1, "244": 3, + "246": 5, "253": 1, - "260": 1, + "260": 2, "261": 3 } } @@ -198212,6 +198597,9 @@ [ "lands", { + "1": { + "251": 1 + }, "4": { "1": 1, "10": 1, @@ -198222,6 +198610,7 @@ "86": 1, "102": 1, "110": 1, + "113": 1, "122": 1, "124": 1, "155": 2, @@ -198237,7 +198626,8 @@ "230": 1, "234": 1, "244": 2, - "246": 1, + "246": 2, + "251": 1, "263": 1, "265": 1 } @@ -198263,6 +198653,7 @@ "223": 1, "226": 1, "227": 1, + "229": 1, "245": 1, "246": 1, "268": 1 @@ -199208,7 +199599,7 @@ "221": 4, "224": 1, "228": 6, - "229": 4, + "229": 5, "230": 7, "232": 4, "236": 3, @@ -199312,10 +199703,11 @@ "221": 5, "223": 1, "226": 7, - "229": 2, + "229": 7, "230": 8, "240": 2, "244": 1, + "246": 1, "261": 2 } } @@ -199352,6 +199744,7 @@ "218": 1, "226": 1, "227": 1, + "229": 2, "237": 1, "238": 1, "240": 1, @@ -199411,11 +199804,12 @@ "230": 1, "240": 1, "244": 1, - "246": 2, + "246": 5, "254": 2, "257": 2, "258": 1, - "260": 3 + "260": 3, + "272": 1 } } ], @@ -199479,6 +199873,7 @@ "231": 2, "235": 1, "240": 1, + "246": 1, "252": 1, "260": 3 } @@ -199635,7 +200030,8 @@ "214": 11, "216": 1, "226": 5, - "246": 2 + "229": 1, + "246": 8 } } ], @@ -200305,7 +200701,7 @@ "107": 6, "108": 1, "111": 1, - "113": 2, + "113": 3, "114": 2, "115": 2, "116": 16, @@ -200475,7 +200871,7 @@ "230": 1, "243": 5, "244": 17, - "246": 1, + "246": 2, "250": 2, "253": 5, "264": 4, @@ -200813,7 +201209,7 @@ "241": 2, "242": 2, "244": 3, - "246": 14, + "246": 19, "248": 18, "249": 1, "250": 13, @@ -201652,6 +202048,7 @@ "267": 2, "269": 1, "270": 2, + "272": 1, "274": 4, "275": 2 } @@ -201806,7 +202203,7 @@ "106": 2, "110": 1, "111": 1, - "113": 1, + "113": 2, "115": 1, "117": 1, "118": 1, @@ -201852,7 +202249,7 @@ "225": 1, "226": 6, "228": 2, - "229": 3, + "229": 4, "230": 3, "232": 3, "233": 3, @@ -202164,7 +202561,7 @@ "225": 6, "226": 16, "227": 3, - "229": 27, + "229": 33, "230": 8, "231": 2, "232": 8, @@ -202304,7 +202701,7 @@ "226": 32, "227": 17, "228": 1, - "229": 47, + "229": 54, "230": 19, "231": 5, "232": 50, @@ -202498,8 +202895,8 @@ "108": 21, "109": 2, "111": 4, - "112": 9, - "113": 3, + "112": 10, + "113": 7, "114": 17, "115": 7, "116": 4, @@ -202611,7 +203008,7 @@ "226": 28, "227": 16, "228": 4, - "229": 5, + "229": 7, "230": 11, "231": 18, "232": 29, @@ -202972,7 +203369,7 @@ "224": 15, "226": 3, "227": 7, - "229": 11, + "229": 12, "230": 18, "231": 4, "232": 11, @@ -202983,6 +203380,7 @@ "239": 10, "240": 9, "244": 4, + "246": 1, "250": 11, "253": 48, "255": 21, @@ -204270,7 +204668,7 @@ "109": 1, "110": 1, "111": 2, - "112": 3, + "112": 4, "113": 7, "114": 9, "116": 1, @@ -205121,10 +205519,11 @@ "188": 5, "217": 1, "218": 2, - "229": 24, + "229": 26, "230": 2, "231": 4, "235": 1, + "246": 4, "250": 2, "254": 3 } @@ -206770,6 +207169,14 @@ } } ], + [ + "indent", + { + "4": { + "113": 3 + } + } + ], [ "indefinite", { @@ -207032,7 +207439,7 @@ "131": 2, "137": 1, "185": 1, - "251": 1 + "251": 2 }, "3": { "15": 1, @@ -207135,7 +207542,7 @@ "245": 1, "249": 1, "250": 11, - "251": 5, + "251": 6, "252": 1, "254": 3, "255": 3, @@ -207752,7 +208159,7 @@ "240": 1, "241": 3, "244": 7, - "246": 12, + "246": 14, "248": 1, "251": 1, "257": 4, @@ -208165,6 +208572,7 @@ "106": 2, "109": 1, "110": 2, + "113": 1, "114": 1, "119": 2, "120": 1, @@ -208216,7 +208624,7 @@ "226": 9, "227": 1, "228": 3, - "229": 8, + "229": 13, "230": 5, "231": 4, "232": 10, @@ -208233,8 +208641,9 @@ "243": 1, "244": 4, "245": 1, - "246": 1, + "246": 2, "250": 3, + "251": 1, "254": 2, "257": 3, "259": 4, @@ -208289,7 +208698,7 @@ "147": 1, "221": 6, "224": 1, - "229": 4, + "229": 5, "230": 12, "252": 2, "259": 1, @@ -208757,6 +209166,7 @@ "94": 3, "95": 1, "104": 3, + "112": 1, "115": 1, "141": 2, "163": 5, @@ -209285,7 +209695,7 @@ "109": 2, "110": 1, "111": 3, - "113": 3, + "113": 4, "115": 7, "117": 3, "119": 4, @@ -209395,7 +209805,7 @@ "257": 3, "258": 1, "259": 2, - "260": 1, + "260": 2, "261": 5, "262": 1, "263": 3, @@ -209403,6 +209813,7 @@ "269": 2, "270": 1, "271": 1, + "272": 1, "273": 2, "274": 3, "275": 2 @@ -209415,7 +209826,9 @@ "4": { "230": 2, "236": 1, - "244": 2 + "244": 2, + "246": 1, + "260": 1 } } ], @@ -209596,9 +210009,9 @@ "214": 2, "221": 1, "223": 1, + "229": 1, "230": 1, "244": 1, - "251": 1, "257": 1, "263": 1, "273": 3, @@ -209946,7 +210359,7 @@ "86": 1, "141": 1, "220": 1, - "229": 1, + "229": 2, "257": 1, "259": 1, "273": 3 @@ -210282,6 +210695,7 @@ "53": 2, "54": 3, "63": 1, + "113": 1, "199": 1, "231": 2, "232": 1 @@ -210587,7 +211001,7 @@ "71": 2, "94": 5, "95": 4, - "113": 1, + "113": 2, "160": 1, "164": 2, "166": 2, @@ -212336,7 +212750,7 @@ "247": 1, "253": 2, "255": 2, - "260": 10, + "260": 12, "266": 1, "267": 2, "268": 1, @@ -212844,7 +213258,7 @@ "106": 3, "107": 1, "112": 2, - "113": 3, + "113": 4, "114": 1, "115": 1, "117": 1, @@ -213797,6 +214211,7 @@ { "4": { "52": 1, + "113": 1, "152": 1, "185": 1, "199": 1, @@ -213878,11 +214293,13 @@ "144": 11, "145": 10, "221": 2, - "229": 39, + "229": 44, "230": 1, "231": 4, "235": 2, "240": 3, + "246": 7, + "251": 1, "252": 2, "260": 4 } @@ -214251,6 +214668,7 @@ "225": 1, "226": 1, "227": 9, + "229": 1, "230": 4, "231": 1, "232": 2, @@ -214818,7 +215236,7 @@ "243": 1, "244": 2, "245": 3, - "246": 3, + "246": 4, "255": 2, "275": 1 } @@ -216230,7 +216648,7 @@ "236": 1, "241": 1, "244": 2, - "246": 2, + "246": 3, "256": 1, "260": 8, "262": 1, @@ -216428,10 +216846,11 @@ "236": 1, "244": 3, "246": 1, - "251": 3, + "251": 4, "255": 1, "257": 1, - "268": 1 + "268": 1, + "272": 1 } } ], @@ -216495,7 +216914,7 @@ "240": 1, "244": 2, "246": 3, - "251": 1, + "251": 2, "257": 1, "260": 1, "262": 1, @@ -216785,6 +217204,7 @@ "123": 1, "152": 1, "179": 1, + "251": 1, "255": 1, "257": 1, "266": 1 @@ -217333,7 +217753,7 @@ "226": 16, "227": 5, "228": 5, - "229": 6, + "229": 9, "230": 3, "231": 4, "232": 12, @@ -217350,6 +217770,7 @@ "248": 3, "249": 3, "250": 2, + "251": 1, "252": 1, "254": 4, "258": 4, @@ -217470,7 +217891,7 @@ "105": 4, "106": 4, "107": 1, - "113": 3, + "113": 5, "114": 2, "115": 4, "118": 5, @@ -217547,7 +217968,7 @@ "225": 1, "226": 7, "228": 3, - "229": 2, + "229": 6, "230": 15, "231": 3, "232": 10, @@ -217569,7 +217990,7 @@ "256": 7, "257": 2, "258": 2, - "260": 2, + "260": 3, "263": 3, "264": 1, "265": 1, @@ -217846,7 +218267,9 @@ "230": 1, "233": 1, "244": 1, + "246": 1, "259": 1, + "260": 1, "261": 1, "262": 2, "263": 3, @@ -217864,7 +218287,7 @@ }, "4": { "249": 8, - "251": 6, + "251": 5, "272": 1 } } @@ -217987,7 +218410,7 @@ "221": 1, "224": 2, "228": 1, - "229": 2, + "229": 3, "231": 1, "233": 2, "240": 1, @@ -218389,6 +218812,7 @@ "237": 1, "238": 1, "244": 1, + "251": 1, "257": 1, "260": 1, "261": 1, @@ -218687,6 +219111,7 @@ "108": 7, "109": 1, "112": 2, + "113": 1, "114": 4, "115": 2, "116": 2, @@ -219177,7 +219602,7 @@ "224": 2, "226": 1, "227": 4, - "229": 1, + "229": 2, "230": 1, "233": 3, "234": 6, @@ -219187,6 +219612,8 @@ "240": 4, "241": 2, "245": 3, + "246": 1, + "251": 2, "254": 1, "255": 1, "259": 1, @@ -219197,7 +219624,7 @@ "266": 3, "270": 1, "271": 2, - "272": 13, + "272": 14, "274": 1 } } @@ -219594,7 +220021,7 @@ "226": 13, "227": 1, "228": 2, - "229": 10, + "229": 11, "230": 9, "231": 3, "232": 3, @@ -221319,7 +221746,7 @@ "60": 1, "84": 4, "112": 4, - "113": 1, + "113": 2, "147": 1, "173": 1, "176": 1, @@ -221761,6 +222188,7 @@ "226": 1, "239": 6, "244": 6, + "251": 1, "254": 1, "264": 1, "272": 12 @@ -222059,6 +222487,7 @@ "102": 1, "103": 1, "104": 4, + "113": 1, "201": 2, "202": 1, "210": 2, @@ -222840,7 +223269,7 @@ "32": 1, "119": 1, "122": 1, - "251": 2 + "251": 1 } } ], @@ -223167,9 +223596,10 @@ "25": 9, "183": 1, "228": 3, + "229": 4, "230": 1, "244": 14, - "246": 54, + "246": 63, "251": 8, "260": 1, "271": 21 @@ -223299,6 +223729,7 @@ "220": 1, "226": 3, "228": 1, + "229": 1, "230": 1, "231": 1, "234": 2, @@ -223308,10 +223739,10 @@ "243": 1, "244": 12, "245": 1, - "246": 12, + "246": 13, "248": 2, "249": 1, - "251": 1, + "251": 2, "258": 4, "260": 2, "261": 3, @@ -223563,7 +223994,7 @@ "225": 5, "226": 13, "228": 23, - "229": 22, + "229": 30, "230": 6, "231": 14, "232": 2, @@ -223577,12 +224008,12 @@ "243": 2, "244": 65, "245": 3, - "246": 89, + "246": 100, "247": 3, "248": 24, "249": 4, "250": 3, - "251": 14, + "251": 18, "252": 1, "253": 1, "255": 3, @@ -223599,7 +224030,7 @@ "269": 2, "270": 6, "271": 53, - "272": 2, + "272": 5, "273": 31, "274": 1 } @@ -224664,6 +225095,7 @@ "214": 1, "221": 1, "223": 3, + "229": 1, "231": 1, "232": 1, "237": 2, @@ -226280,7 +226712,7 @@ "108": 3, "109": 4, "110": 4, - "113": 8, + "113": 12, "114": 7, "115": 8, "116": 13, @@ -226378,7 +226810,7 @@ "225": 1, "226": 3, "227": 6, - "229": 8, + "229": 9, "230": 15, "231": 7, "232": 4, @@ -226394,11 +226826,11 @@ "243": 3, "244": 8, "245": 2, - "246": 3, + "246": 5, "248": 4, "249": 11, "250": 7, - "251": 1, + "251": 2, "253": 11, "254": 2, "255": 5, @@ -226417,7 +226849,7 @@ "268": 4, "269": 2, "270": 16, - "272": 2, + "272": 4, "273": 2, "274": 2, "275": 2 @@ -227966,7 +228398,7 @@ "243": 1, "244": 9, "245": 4, - "246": 8, + "246": 9, "257": 1, "259": 1, "266": 2, @@ -228506,6 +228938,7 @@ "94": 5, "99": 7, "100": 4, + "113": 1, "114": 1, "115": 1, "117": 1, @@ -228796,7 +229229,7 @@ "267": 1, "270": 1, "271": 4, - "272": 1 + "272": 2 } } ], @@ -228945,7 +229378,7 @@ "231": 2, "236": 3, "244": 1, - "246": 1, + "246": 2, "252": 1, "259": 1, "261": 1, @@ -229049,7 +229482,7 @@ "218": 1, "235": 1, "244": 2, - "251": 4, + "251": 5, "271": 1, "272": 1, "274": 3 @@ -229995,6 +230428,7 @@ "106": 2, "107": 1, "111": 2, + "113": 1, "114": 3, "115": 6, "116": 7, @@ -230057,7 +230491,7 @@ "223": 1, "225": 1, "226": 3, - "229": 2, + "229": 3, "231": 1, "239": 3, "242": 1, @@ -230219,7 +230653,7 @@ "87": 2, "94": 1, "98": 1, - "113": 1, + "113": 2, "123": 1, "139": 1, "145": 1, @@ -230362,7 +230796,7 @@ "107": 2, "109": 1, "110": 1, - "112": 2, + "112": 3, "113": 6, "114": 1, "115": 2, @@ -230433,6 +230867,7 @@ "241": 3, "244": 2, "245": 1, + "246": 1, "248": 1, "249": 1, "250": 2, @@ -230581,7 +231016,7 @@ "84": 1, "90": 2, "112": 1, - "113": 1, + "113": 3, "119": 1, "128": 1, "165": 2, @@ -231139,7 +231574,7 @@ "232": 1, "236": 1, "238": 2, - "246": 3, + "246": 7, "260": 2, "265": 1 } @@ -231274,7 +231709,7 @@ "222": 1, "223": 7, "226": 3, - "229": 50, + "229": 55, "231": 5, "235": 3, "236": 4, @@ -231282,7 +231717,7 @@ "238": 5, "240": 3, "241": 2, - "246": 6, + "246": 21, "251": 3, "254": 4, "256": 2, @@ -231329,6 +231764,7 @@ "68": 1, "79": 1, "94": 1, + "112": 1, "114": 9, "115": 1, "135": 3, @@ -232272,14 +232708,6 @@ } } ], - [ - "disc", - { - "4": { - "257": 1 - } - } - ], [ "discharg", { @@ -232288,6 +232716,15 @@ } } ], + [ + "disc", + { + "4": { + "229": 1, + "257": 1 + } + } + ], [ "discernibility", { @@ -233451,6 +233888,22 @@ } } ], + [ + "1398", + { + "4": { + "246": 2 + } + } + ], + [ + "139", + { + "4": { + "113": 1 + } + } + ], [ "133", { @@ -233620,7 +234073,8 @@ "185", { "4": { - "152": 2 + "152": 2, + "246": 1 } } ], @@ -233679,6 +234133,7 @@ "162": 1, "186": 1, "244": 1, + "246": 1, "261": 1, "262": 2 } @@ -233857,6 +234312,7 @@ "243": 1, "244": 3, "245": 4, + "246": 1, "247": 2, "248": 1, "250": 2, @@ -233987,6 +234443,7 @@ "35": 1, "104": 2, "106": 1, + "113": 1, "116": 1, "123": 1, "125": 3, @@ -234120,7 +234577,7 @@ "1206", { "4": { - "246": 1 + "246": 3 } } ], @@ -234886,6 +235343,14 @@ } } ], + [ + "1668", + { + "4": { + "246": 2 + } + } + ], [ "1668x2420", { @@ -235048,7 +235513,7 @@ "110": 5, "111": 2, "112": 2, - "113": 6, + "113": 7, "114": 1, "115": 3, "116": 7, @@ -235146,7 +235611,8 @@ "17e", { "4": { - "210": 1 + "210": 1, + "229": 1 } } ], @@ -235360,22 +235826,22 @@ "225": 1, "226": 7, "227": 2, - "229": 1, + "229": 3, "230": 3, "232": 6, "236": 7, "237": 2, "238": 5, "241": 2, - "246": 2, + "246": 3, "250": 1, - "251": 1, + "251": 2, "252": 1, "254": 4, "255": 7, "256": 4, "259": 6, - "260": 1, + "260": 2, "261": 7, "262": 7, "263": 14, @@ -235489,8 +235955,11 @@ "173": 3, "210": 2, "214": 1, + "246": 1, + "251": 1, "262": 2, - "263": 5 + "263": 5, + "272": 1 } } ], @@ -235975,9 +236444,11 @@ "230": 1, "234": 1, "240": 4, + "246": 1, "250": 1, "252": 1, "253": 1, + "260": 1, "262": 2, "263": 9, "265": 3, @@ -236446,7 +236917,9 @@ { "4": { "94": 2, - "166": 1 + "166": 1, + "246": 1, + "260": 1 } } ], @@ -239152,7 +239625,7 @@ "9": 1, "13": 1, "104": 1, - "112": 2, + "112": 3, "181": 1, "263": 2 } @@ -239995,7 +240468,7 @@ "239": 5, "240": 1, "244": 2, - "246": 17, + "246": 18, "249": 3, "251": 2, "258": 1, @@ -240166,10 +240639,10 @@ "243": 6, "244": 8, "245": 1, - "246": 2, + "246": 3, "248": 1, "250": 14, - "251": 2, + "251": 3, "253": 2, "256": 1, "258": 1, @@ -240184,7 +240657,7 @@ "269": 1, "270": 1, "271": 2, - "272": 2, + "272": 1, "273": 2, "274": 3, "275": 4 @@ -240250,7 +240723,7 @@ "221": 6, "224": 4, "227": 3, - "229": 38, + "229": 47, "230": 15, "231": 6, "233": 2, @@ -240273,7 +240746,7 @@ "4": { "0": 1, "2": 2, - "229": 5, + "229": 6, "230": 1, "235": 4, "237": 1 @@ -240297,18 +240770,17 @@ "91": 1, "92": 2, "93": 1, + "112": 1, "205": 1, "206": 1, - "251": 2 + "246": 2, + "251": 1 } } ], [ "verbatim", { - "1": { - "251": 1 - }, "4": { "15": 1, "20": 1, @@ -240321,7 +240793,6 @@ "162": 1, "172": 4, "174": 1, - "251": 2, "273": 1 } } @@ -240792,6 +241263,7 @@ { "4": { "11": 1, + "113": 1, "251": 1 } } @@ -241286,7 +241758,8 @@ "108": 6, "109": 2, "111": 2, - "113": 20, + "112": 1, + "113": 28, "114": 6, "115": 4, "116": 4, @@ -241366,7 +241839,7 @@ "225": 13, "226": 21, "227": 88, - "229": 5, + "229": 6, "230": 28, "231": 5, "232": 28, @@ -241400,7 +241873,7 @@ "269": 10, "270": 2, "271": 1, - "272": 3, + "272": 2, "273": 20 } } @@ -242286,7 +242759,7 @@ "226": 59, "227": 36, "228": 4, - "229": 21, + "229": 22, "230": 38, "231": 12, "232": 38, @@ -242606,6 +243079,7 @@ "16": 1, "57": 1, "58": 1, + "113": 1, "135": 2, "170": 1, "172": 1, @@ -243346,7 +243820,7 @@ "226": 155, "227": 41, "228": 12, - "229": 101, + "229": 105, "230": 79, "231": 71, "232": 142, @@ -243363,8 +243837,9 @@ "243": 2, "244": 23, "245": 2, - "246": 1, + "246": 2, "247": 8, + "251": 3, "252": 23, "253": 110, "254": 8, @@ -243384,6 +243859,7 @@ "268": 6, "269": 7, "270": 1, + "272": 1, "275": 3 } } @@ -243590,6 +244066,7 @@ "225": 2, "226": 5, "227": 1, + "229": 1, "230": 6, "231": 2, "232": 5, @@ -243602,6 +244079,7 @@ "240": 5, "244": 2, "245": 3, + "251": 2, "253": 2, "255": 3, "256": 5, @@ -243613,7 +244091,7 @@ "265": 2, "266": 2, "269": 1, - "272": 3 + "272": 5 } } ], @@ -243654,7 +244132,7 @@ "221": 16, "224": 2, "226": 1, - "229": 4, + "229": 5, "230": 5, "231": 3, "232": 11, @@ -244153,7 +244631,7 @@ "243": 2, "244": 16, "245": 4, - "246": 8, + "246": 9, "251": 2, "261": 2, "262": 2, @@ -244997,7 +245475,7 @@ "107": 5, "108": 6, "112": 2, - "113": 13, + "113": 14, "114": 5, "115": 18, "116": 1, @@ -245078,7 +245556,7 @@ "244": 1, "246": 2, "250": 3, - "251": 1, + "251": 2, "253": 9, "254": 1, "256": 1, @@ -245092,6 +245570,7 @@ "268": 1, "270": 1, "271": 4, + "272": 1, "273": 1, "274": 5, "275": 3 @@ -245414,6 +245893,7 @@ "86": 1, "90": 1, "94": 1, + "113": 1, "115": 1, "119": 2, "136": 2, @@ -245454,7 +245934,8 @@ "39": 1, "40": 3, "44": 1, - "52": 1 + "52": 1, + "113": 2 } } ], @@ -245472,6 +245953,7 @@ "46": 1, "48": 1, "56": 6, + "113": 1, "119": 1, "131": 1, "137": 2, @@ -245638,7 +246120,7 @@ "107": 2, "108": 2, "112": 1, - "113": 2, + "113": 5, "114": 2, "115": 1, "134": 2, @@ -245757,7 +246239,7 @@ "107": 1, "108": 5, "112": 1, - "113": 1, + "113": 2, "114": 3, "118": 1, "119": 1, @@ -246923,7 +247405,7 @@ "244": 54, "245": 30, "247": 1, - "251": 6, + "251": 7, "254": 1, "257": 3, "262": 1, @@ -247923,6 +248405,7 @@ "96": 1, "104": 1, "106": 2, + "113": 1, "118": 2, "119": 1, "125": 1, @@ -248003,6 +248486,7 @@ "206": 1, "224": 1, "231": 2, + "246": 1, "254": 1, "262": 2, "265": 1 @@ -248209,6 +248693,7 @@ "102": 2, "104": 1, "109": 2, + "113": 1, "118": 2, "119": 2, "122": 1, @@ -248547,6 +249032,8 @@ "103": 1, "106": 2, "111": 1, + "112": 1, + "113": 1, "114": 1, "115": 1, "116": 1, @@ -248609,7 +249096,7 @@ "225": 1, "226": 4, "227": 1, - "229": 8, + "229": 10, "230": 4, "231": 3, "232": 1, @@ -248621,10 +249108,10 @@ "241": 1, "244": 4, "245": 1, - "246": 4, + "246": 7, "249": 1, "250": 2, - "251": 4, + "251": 5, "257": 1, "258": 1, "260": 2, @@ -248635,6 +249122,7 @@ "269": 3, "270": 5, "271": 7, + "272": 1, "273": 1, "274": 1, "275": 1 @@ -248650,6 +249138,7 @@ "24": 1, "108": 1, "109": 1, + "113": 2, "114": 1, "134": 1, "162": 1, @@ -248657,7 +249146,8 @@ "223": 1, "226": 1, "231": 1, - "249": 1 + "249": 1, + "272": 1 } } ], @@ -248761,8 +249251,8 @@ "107": 5, "110": 1, "111": 1, - "112": 1, - "113": 1, + "112": 3, + "113": 4, "114": 2, "115": 4, "117": 2, @@ -248838,7 +249328,7 @@ "224": 3, "225": 5, "226": 5, - "229": 8, + "229": 9, "230": 6, "231": 4, "233": 1, @@ -248851,7 +249341,7 @@ "243": 1, "244": 4, "245": 1, - "246": 2, + "246": 4, "247": 4, "249": 2, "250": 2, @@ -248873,7 +249363,7 @@ "269": 2, "270": 2, "271": 9, - "272": 2, + "272": 3, "273": 11, "274": 3, "275": 2 @@ -249031,7 +249521,7 @@ "221": 3, "222": 1, "226": 1, - "229": 2, + "229": 3, "230": 1, "237": 1, "239": 1, @@ -249169,7 +249659,7 @@ "108": 1, "111": 4, "112": 1, - "113": 2, + "113": 3, "115": 1, "116": 1, "117": 2, @@ -249246,7 +249736,7 @@ "225": 1, "226": 3, "227": 3, - "229": 11, + "229": 15, "230": 9, "231": 2, "232": 1, @@ -249259,6 +249749,7 @@ "244": 7, "246": 3, "250": 2, + "251": 2, "254": 2, "257": 2, "259": 1, @@ -249689,8 +250180,8 @@ "109": 3, "110": 7, "111": 5, - "112": 11, - "113": 6, + "112": 13, + "113": 7, "114": 9, "115": 13, "116": 8, @@ -249802,7 +250293,7 @@ "226": 32, "227": 16, "228": 4, - "229": 24, + "229": 23, "230": 16, "231": 25, "232": 15, @@ -249818,12 +250309,12 @@ "242": 7, "243": 1, "244": 10, - "246": 9, + "246": 10, "247": 2, "248": 3, "249": 7, "250": 6, - "251": 10, + "251": 11, "252": 4, "253": 8, "254": 21, @@ -249844,7 +250335,7 @@ "269": 18, "270": 16, "271": 12, - "272": 1, + "272": 2, "273": 4, "274": 12, "275": 6 @@ -249929,6 +250420,7 @@ "96": 1, "101": 1, "111": 1, + "113": 1, "119": 1, "133": 1, "136": 1, @@ -249998,7 +250490,7 @@ "101": 1, "102": 1, "106": 2, - "113": 1, + "113": 3, "119": 1, "122": 2, "141": 1, @@ -250031,6 +250523,7 @@ "268": 1, "270": 1, "271": 1, + "272": 1, "273": 3, "275": 1 } @@ -250139,7 +250632,7 @@ "217": 1, "222": 1, "226": 1, - "229": 1, + "229": 2, "230": 2, "235": 1, "244": 2, @@ -250261,8 +250754,8 @@ "106": 2, "108": 2, "110": 1, - "112": 5, - "113": 2, + "112": 6, + "113": 3, "114": 3, "115": 4, "117": 3, @@ -250361,7 +250854,7 @@ "226": 9, "227": 2, "228": 5, - "229": 6, + "229": 8, "230": 14, "231": 2, "232": 3, @@ -250377,12 +250870,12 @@ "243": 2, "244": 10, "245": 1, - "246": 8, + "246": 11, "247": 7, "248": 5, "249": 1, "250": 2, - "251": 2, + "251": 3, "254": 1, "256": 1, "258": 5, @@ -250494,7 +250987,7 @@ "231": 1, "236": 2, "244": 1, - "251": 2, + "251": 3, "257": 1, "264": 1, "269": 1, @@ -250788,7 +251281,7 @@ "80": 1, "92": 1, "93": 1, - "113": 2, + "113": 5, "115": 1, "134": 2, "146": 1, @@ -251248,7 +251741,7 @@ "226": 10, "227": 5, "228": 1, - "229": 1, + "229": 2, "230": 1, "232": 16, "233": 1, @@ -251261,10 +251754,10 @@ "243": 3, "244": 8, "245": 1, - "246": 8, + "246": 9, "247": 1, "248": 2, - "251": 1, + "251": 2, "253": 2, "254": 6, "256": 1, @@ -251471,7 +251964,7 @@ "226": 2, "227": 7, "228": 1, - "229": 2, + "229": 3, "230": 5, "231": 2, "232": 5, @@ -252201,7 +252694,7 @@ "110": 2, "111": 1, "112": 3, - "113": 3, + "113": 4, "114": 4, "115": 7, "116": 2, @@ -252303,7 +252796,7 @@ "226": 3, "227": 3, "228": 2, - "229": 4, + "229": 5, "230": 4, "231": 2, "232": 6, @@ -252322,7 +252815,7 @@ "248": 3, "249": 2, "250": 4, - "251": 3, + "251": 5, "253": 2, "254": 6, "255": 2, @@ -252538,7 +253031,7 @@ "225": 2, "226": 2, "228": 1, - "229": 24, + "229": 26, "230": 33, "231": 32, "232": 10, @@ -252547,13 +253040,13 @@ "240": 13, "241": 3, "244": 5, - "246": 2, + "246": 3, "249": 4, "252": 3, "253": 4, "255": 1, "256": 1, - "260": 36, + "260": 39, "264": 2, "267": 2, "268": 6, @@ -253232,7 +253725,7 @@ "231": 12, "235": 1, "244": 1, - "246": 1, + "246": 2, "247": 1, "249": 2, "252": 2, @@ -253325,7 +253818,8 @@ "240": 4, "241": 1, "244": 2, - "251": 1, + "246": 1, + "251": 2, "252": 13, "253": 65, "255": 11, @@ -253336,7 +253830,7 @@ "261": 12, "262": 2, "263": 2, - "272": 1, + "272": 2, "273": 1 } } @@ -253526,7 +254020,8 @@ { "4": { "146": 1, - "147": 4 + "147": 4, + "246": 4 } } ], @@ -253647,7 +254142,7 @@ "219": 7, "226": 5, "245": 5, - "246": 8, + "246": 11, "251": 1, "272": 9 } @@ -254379,7 +254874,8 @@ "144": 1, "149": 1, "164": 1, - "204": 2 + "204": 2, + "251": 1 } } ], @@ -254389,7 +254885,8 @@ "1": { "108": 2, "120": 1, - "246": 1 + "246": 1, + "251": 1 }, "3": { "192": 1, @@ -254437,16 +254934,18 @@ "225": 2, "226": 2, "228": 1, - "229": 1, + "229": 3, "230": 2, "231": 4, "237": 1, "244": 4, "246": 18, "248": 6, + "251": 12, "263": 2, "268": 2, "271": 3, + "272": 21, "273": 1 } } @@ -254495,6 +254994,7 @@ "57": 1, "91": 8, "93": 48, + "112": 1, "113": 5, "153": 2 } @@ -254513,8 +255013,8 @@ "92": 1, "93": 13, "104": 1, - "112": 3, - "113": 1 + "112": 4, + "113": 2 } } ], @@ -254534,7 +255034,7 @@ "16": 1, "91": 1, "92": 2, - "112": 2, + "112": 3, "153": 1, "251": 1 } @@ -254915,7 +255415,7 @@ "226": 2, "227": 2, "228": 2, - "229": 33, + "229": 34, "230": 28, "231": 21, "232": 11, @@ -254930,13 +255430,13 @@ "243": 3, "244": 13, "245": 4, - "246": 5, + "246": 11, "248": 1, - "251": 1, + "251": 4, "252": 1, "253": 2, "258": 5, - "260": 15, + "260": 17, "261": 2, "262": 16, "263": 8, @@ -255218,7 +255718,9 @@ "65": 2, "89": 1, "111": 1, - "148": 2 + "148": 2, + "229": 2, + "251": 1 } } ], @@ -255254,6 +255756,7 @@ "python3", { "4": { + "112": 1, "204": 1 } } @@ -255524,9 +256027,11 @@ "210": 1, "226": 1, "246": 1, + "251": 1, "261": 3, "262": 1, - "263": 1 + "263": 1, + "272": 1 } } ], @@ -255741,11 +256246,13 @@ "31": 1, "40": 1, "95": 1, + "113": 1, "147": 1, "158": 3, "170": 2, "172": 2, - "178": 1 + "178": 1, + "251": 1 } } ], @@ -255785,6 +256292,7 @@ "106": 2, "107": 1, "108": 1, + "113": 1, "119": 1, "120": 5, "126": 1, @@ -255884,7 +256392,8 @@ "3": { "45": 1, "170": 1, - "266": 1 + "266": 1, + "272": 1 }, "4": { "0": 2, @@ -255922,6 +256431,7 @@ "104": 1, "110": 1, "112": 1, + "113": 1, "114": 1, "119": 1, "121": 1, @@ -255966,7 +256476,7 @@ "237": 2, "240": 2, "244": 1, - "246": 4, + "246": 3, "249": 1, "250": 1, "256": 1, @@ -255978,6 +256488,7 @@ "268": 1, "269": 1, "270": 3, + "272": 2, "274": 3 } } @@ -256680,6 +257191,14 @@ } } ], + [ + "punctua", + { + "4": { + "113": 1 + } + } + ], [ "punchline", { @@ -257054,6 +257573,7 @@ "75": 3, "106": 1, "107": 1, + "113": 1, "130": 2, "139": 3, "166": 1, @@ -257080,6 +257600,7 @@ "181": 1, "218": 1, "228": 1, + "229": 1, "230": 3, "239": 1, "251": 1, @@ -257139,7 +257660,7 @@ "223": 3, "224": 1, "228": 1, - "229": 4, + "229": 5, "232": 1, "233": 3, "236": 1, @@ -257239,6 +257760,7 @@ "223": 2, "232": 9, "233": 1, + "251": 1, "265": 1, "268": 1 } @@ -259481,7 +260003,6 @@ "227": 10, "240": 1, "244": 1, - "251": 1, "260": 10, "262": 2, "263": 6, @@ -259607,6 +260128,7 @@ "244": 1, "247": 3, "249": 1, + "251": 1, "255": 2, "257": 1, "261": 5, @@ -259680,6 +260202,7 @@ "101": 1, "104": 2, "106": 1, + "113": 4, "114": 1, "115": 1, "118": 1, @@ -259741,6 +260264,7 @@ "247": 2, "248": 1, "250": 2, + "251": 1, "252": 1, "255": 16, "256": 1, @@ -259753,7 +260277,7 @@ "268": 4, "269": 1, "271": 1, - "272": 4, + "272": 6, "273": 1 } } @@ -259866,7 +260390,8 @@ "202": 3, "203": 2, "220": 4, - "244": 1 + "244": 1, + "246": 5 } } ], @@ -259893,12 +260418,13 @@ "147": 3, "157": 1, "210": 2, - "229": 6, + "229": 8, "230": 4, "231": 4, "233": 1, "236": 6, "244": 1, + "246": 4, "253": 1, "260": 2, "261": 3, @@ -260887,7 +261413,8 @@ { "4": { "221": 2, - "229": 3, + "229": 5, + "246": 1, "261": 1, "262": 5, "263": 1 @@ -260912,7 +261439,8 @@ "149": 4, "151": 2, "221": 11, - "229": 11, + "229": 14, + "246": 1, "255": 10, "256": 6, "261": 15, @@ -261473,6 +262001,7 @@ "plutil", { "4": { + "112": 2, "132": 1, "195": 2, "196": 2, @@ -261682,7 +262211,7 @@ "246": 4, "249": 2, "250": 1, - "251": 2, + "251": 3, "255": 1, "260": 2, "263": 1, @@ -261694,6 +262223,14 @@ } } ], + [ + "plists", + { + "4": { + "112": 1 + } + } + ], [ "plist", { @@ -261871,7 +262408,7 @@ "128": 1, "189": 1, "211": 1, - "229": 3, + "229": 5, "237": 1, "240": 1 } @@ -262039,7 +262576,7 @@ "225": 1, "226": 2, "228": 2, - "229": 10, + "229": 11, "230": 9, "231": 8, "232": 5, @@ -262346,7 +262883,7 @@ "103": 1, "104": 4, "106": 1, - "112": 8, + "112": 9, "115": 1, "118": 2, "119": 1, @@ -262477,6 +263014,7 @@ "105": 1, "106": 4, "107": 1, + "113": 1, "115": 5, "118": 1, "122": 1, @@ -262828,7 +263366,7 @@ "205": 1, "220": 39, "224": 5, - "229": 7, + "229": 9, "230": 1, "231": 3, "233": 3, @@ -262918,6 +263456,7 @@ "75": 3, "77": 3, "100": 1, + "113": 4, "116": 1, "118": 4, "119": 15, @@ -263606,7 +264145,7 @@ "76": 1, "79": 1, "92": 1, - "112": 4, + "112": 5, "169": 1, "171": 1, "172": 1, @@ -263941,6 +264480,7 @@ { "4": { "221": 3, + "229": 1, "266": 1 } } @@ -264793,6 +265333,7 @@ "108": 1, "110": 1, "111": 2, + "113": 2, "114": 2, "115": 1, "116": 1, @@ -266102,13 +266643,15 @@ "246": 1, "249": 2, "250": 1, - "251": 5, + "251": 9, "252": 2, "259": 1, + "260": 1, "261": 4, "263": 2, "269": 1, "270": 1, + "272": 1, "273": 2, "274": 4, "275": 2 @@ -266321,6 +266864,7 @@ "45": 1, "54": 1, "81": 6, + "113": 4, "127": 1, "152": 3, "174": 1, @@ -266386,6 +266930,7 @@ "246": 1, "249": 2, "250": 1, + "251": 1, "263": 1, "275": 1 } @@ -267490,7 +268035,7 @@ "221": 12, "226": 2, "227": 3, - "229": 2, + "229": 3, "230": 17, "231": 24, "232": 17, @@ -267504,13 +268049,13 @@ "240": 11, "243": 1, "244": 15, - "246": 4, + "246": 7, "249": 2, "252": 1, "255": 7, "256": 9, "257": 1, - "260": 9, + "260": 12, "262": 1, "263": 9, "268": 1, @@ -267734,6 +268279,22 @@ } } ], + [ + "pruned", + { + "4": { + "113": 1 + } + } + ], + [ + "prunes", + { + "4": { + "113": 1 + } + } + ], [ "pruner", { @@ -268159,7 +268720,7 @@ "158": 1, "249": 1, "250": 2, - "251": 1 + "251": 2 } } ], @@ -268592,6 +269153,7 @@ "29": 1, "45": 1, "107": 1, + "113": 1, "152": 2, "153": 1, "161": 1, @@ -268627,6 +269189,7 @@ "100": 1, "106": 1, "112": 1, + "113": 2, "118": 1, "119": 1, "130": 1, @@ -270414,6 +270977,7 @@ "210": 11, "211": 1, "221": 4, + "229": 2, "230": 4, "231": 1, "236": 2, @@ -270422,13 +270986,13 @@ "243": 1, "244": 16, "245": 4, - "246": 1, + "246": 2, "248": 1, "253": 1, "255": 1, "256": 5, "258": 3, - "260": 1, + "260": 2, "262": 1 } } @@ -270538,6 +271102,7 @@ "255": 2, "256": 1, "257": 2, + "260": 1, "262": 1, "263": 2, "275": 1 @@ -272442,7 +273007,7 @@ "220": 2, "223": 1, "227": 3, - "229": 4, + "229": 5, "230": 5, "235": 1, "239": 1, @@ -272589,7 +273154,7 @@ "220": 3, "221": 1, "227": 1, - "229": 7, + "229": 8, "233": 1, "234": 3, "237": 3, @@ -272892,6 +273457,7 @@ "35": 1, "178": 1, "226": 2, + "251": 1, "255": 1, "256": 1 } @@ -272933,12 +273499,15 @@ "225": 2, "226": 2, "228": 1, + "229": 1, "232": 1, - "246": 3, + "246": 4, "248": 1, + "251": 3, "254": 1, "263": 4, - "271": 1 + "271": 1, + "272": 3 } } ], @@ -273003,6 +273572,8 @@ "106": 3, "107": 9, "108": 7, + "112": 1, + "113": 1, "114": 1, "115": 8, "116": 1, @@ -273062,6 +273633,7 @@ "262": 7, "263": 7, "271": 1, + "272": 1, "274": 1, "275": 1 } @@ -273413,7 +273985,7 @@ "premultiplied", { "4": { - "246": 1 + "246": 2 } } ], @@ -273758,6 +274330,14 @@ } } ], + [ + "precedent", + { + "4": { + "112": 1 + } + } + ], [ "precedes", { @@ -274007,7 +274587,7 @@ "220": 8, "222": 1, "227": 1, - "229": 5, + "229": 7, "230": 1, "231": 2, "236": 3, @@ -275023,6 +275603,7 @@ "101": 1, "104": 2, "105": 3, + "113": 1, "114": 3, "115": 2, "116": 2, @@ -275155,6 +275736,7 @@ "67": 2, "74": 1, "89": 1, + "113": 1, "114": 1, "158": 1, "159": 1, @@ -275334,6 +275916,7 @@ "233": 3, "239": 1, "240": 1, + "251": 1, "259": 14, "262": 1, "272": 6, @@ -277489,7 +278072,7 @@ "226": 12, "227": 5, "228": 7, - "229": 21, + "229": 23, "230": 14, "231": 15, "232": 2, @@ -277506,7 +278089,7 @@ "243": 2, "244": 6, "245": 2, - "246": 7, + "246": 9, "247": 8, "248": 3, "249": 1, @@ -277530,6 +278113,7 @@ "269": 9, "270": 6, "271": 5, + "272": 1, "273": 5, "274": 6, "275": 6 @@ -279735,6 +280319,7 @@ "84": 1, "108": 1, "111": 1, + "113": 1, "137": 1, "145": 1, "163": 2, @@ -279745,6 +280330,7 @@ "218": 1, "239": 1, "244": 1, + "251": 1, "261": 3, "270": 1 } @@ -279828,7 +280414,7 @@ "107": 5, "108": 3, "112": 1, - "113": 6, + "113": 7, "114": 4, "115": 25, "116": 1, @@ -280891,7 +281477,7 @@ "106": 8, "107": 1, "112": 3, - "113": 5, + "113": 6, "115": 1, "117": 1, "119": 1, @@ -281113,6 +281699,7 @@ "221": 2, "222": 4, "224": 2, + "229": 1, "233": 5, "234": 7, "236": 3, @@ -281121,6 +281708,7 @@ "243": 1, "244": 6, "245": 1, + "246": 1, "247": 2, "257": 2, "260": 2, @@ -281249,7 +281837,7 @@ "58": 5, "75": 1, "106": 1, - "112": 10, + "112": 14, "135": 5, "141": 4, "146": 2, @@ -281391,6 +281979,7 @@ "109": 3, "110": 3, "111": 2, + "113": 1, "114": 3, "115": 14, "116": 3, @@ -281504,7 +282093,7 @@ "243": 1, "244": 6, "245": 2, - "246": 3, + "246": 4, "248": 3, "249": 2, "250": 1, @@ -281689,6 +282278,7 @@ "73": 1, "74": 1, "88": 1, + "113": 1, "190": 1, "217": 6, "219": 1, @@ -282005,7 +282595,7 @@ "57": 1, "92": 2, "104": 2, - "112": 1, + "112": 2, "164": 1, "167": 1, "170": 1, @@ -282067,6 +282657,7 @@ "4": { "43": 1, "59": 1, + "113": 1, "120": 2, "131": 1, "151": 1, @@ -282600,7 +283191,7 @@ "200": 2, "220": 1, "227": 1, - "246": 4, + "246": 8, "252": 1, "253": 10, "255": 3, @@ -282628,7 +283219,9 @@ "182": 1, "199": 1, "225": 1, + "229": 1, "232": 1, + "246": 1, "262": 4, "263": 6, "268": 1 @@ -282712,10 +283305,12 @@ "219": 1, "226": 2, "228": 1, + "229": 1, "230": 4, "237": 2, "239": 1, "250": 1, + "251": 1, "254": 1, "258": 1, "262": 6, @@ -282747,6 +283342,7 @@ "41": 1, "63": 2, "82": 1, + "113": 1, "115": 1, "132": 1, "142": 1, @@ -283058,7 +283654,8 @@ "247": 1, "249": 3, "263": 4, - "264": 1 + "264": 1, + "272": 1 } } ], @@ -283243,6 +283840,7 @@ "110": 1, "111": 2, "112": 1, + "113": 4, "115": 7, "117": 1, "139": 1, @@ -283295,6 +283893,7 @@ "248": 2, "249": 1, "250": 2, + "251": 1, "262": 2, "265": 1, "266": 1, @@ -283397,7 +283996,7 @@ "107": 1, "111": 18, "112": 5, - "113": 1, + "113": 2, "115": 2, "116": 4, "117": 1, @@ -283738,6 +284337,7 @@ "105": 1, "106": 3, "109": 2, + "113": 1, "115": 1, "116": 3, "117": 4, @@ -284109,7 +284709,7 @@ "92": 1, "94": 1, "108": 1, - "113": 2, + "113": 3, "118": 1, "119": 1, "121": 1, @@ -284128,6 +284728,7 @@ "215": 1, "221": 3, "222": 1, + "229": 1, "239": 2, "264": 1 } @@ -284170,7 +284771,7 @@ "96": 1, "103": 6, "108": 2, - "113": 1, + "113": 3, "123": 8, "139": 2, "146": 1, @@ -284325,7 +284926,7 @@ "105": 1, "107": 2, "109": 1, - "112": 1, + "112": 2, "113": 1, "114": 3, "115": 5, @@ -284411,7 +285012,7 @@ "244": 2, "245": 1, "249": 2, - "251": 1, + "251": 2, "253": 1, "255": 1, "256": 3, @@ -284927,7 +285528,8 @@ "manag", { "1": { - "232": 1 + "232": 1, + "251": 1 }, "3": { "90": 1 @@ -284955,7 +285557,8 @@ "225": 1, "226": 3, "227": 1, - "232": 1 + "232": 1, + "251": 1 } } ], @@ -285065,7 +285668,7 @@ "200": 1, "202": 1, "210": 3, - "229": 4, + "229": 8, "254": 1, "265": 1, "274": 1 @@ -285217,8 +285820,7 @@ "196": 1, "197": 1, "199": 1, - "227": 1, - "251": 1 + "227": 1 }, "3": { "16": 1, @@ -285316,7 +285918,7 @@ "234": 1, "239": 1, "241": 1, - "251": 2, + "251": 1, "254": 2, "259": 2, "263": 1, @@ -285839,6 +286441,7 @@ "108": 3, "110": 1, "112": 2, + "113": 1, "117": 2, "119": 1, "121": 2, @@ -286337,7 +286940,7 @@ "110": 4, "111": 2, "112": 1, - "113": 1, + "113": 2, "114": 2, "116": 1, "118": 14, @@ -286665,7 +287268,7 @@ "107": 2, "109": 1, "110": 2, - "112": 3, + "112": 4, "115": 1, "116": 1, "117": 2, @@ -287369,6 +287972,14 @@ } } ], + [ + "morceaux", + { + "4": { + "112": 1 + } + } + ], [ "morphs", { @@ -287579,6 +288190,7 @@ "240": 2, "241": 1, "244": 3, + "251": 1, "254": 1, "257": 2, "258": 1, @@ -287591,7 +288203,7 @@ "268": 3, "270": 1, "271": 1, - "272": 1, + "272": 3, "273": 1 } } @@ -287647,10 +288259,12 @@ "222": 1, "227": 1, "228": 1, + "229": 1, "233": 1, "236": 1, "237": 1, "239": 2, + "246": 1, "251": 3, "260": 6 } @@ -288302,6 +288916,7 @@ "235": 2, "240": 3, "244": 1, + "246": 1, "252": 3, "260": 2, "266": 2, @@ -288365,6 +288980,7 @@ "84": 1, "92": 1, "98": 2, + "113": 1, "118": 1, "120": 2, "122": 2, @@ -288763,7 +289379,7 @@ "241": 27, "242": 6, "244": 6, - "246": 1, + "246": 2, "247": 1, "250": 1, "254": 5, @@ -288772,7 +289388,7 @@ "257": 2, "258": 4, "259": 4, - "260": 2, + "260": 3, "261": 2, "262": 10, "263": 11, @@ -289073,7 +289689,7 @@ "249": 2, "257": 2, "258": 6, - "260": 3, + "260": 4, "261": 1, "267": 1, "271": 23, @@ -289233,7 +289849,7 @@ "217": 1, "219": 1, "227": 1, - "229": 3, + "229": 4, "230": 2, "231": 1, "240": 1, @@ -290162,6 +290778,7 @@ "33": 4, "34": 2, "42": 2, + "113": 1, "139": 2, "181": 7, "248": 1 @@ -290188,7 +290805,7 @@ "55": 1, "56": 2, "59": 1, - "113": 2, + "113": 3, "147": 1, "151": 1, "161": 1, @@ -290767,6 +291384,7 @@ "60": 1, "106": 1, "112": 4, + "113": 3, "145": 1, "152": 15, "164": 2, @@ -290782,12 +291400,16 @@ "221": 1, "225": 1, "226": 1, - "229": 4, + "229": 16, "230": 8, "231": 7, "235": 1, "239": 2, - "263": 2 + "246": 1, + "251": 2, + "260": 1, + "263": 2, + "272": 3 } } ], @@ -290859,11 +291481,14 @@ "230": 2, "232": 1, "239": 12, + "246": 1, "249": 2, + "251": 1, "252": 1, "257": 1, "262": 1, - "263": 1 + "263": 1, + "272": 1 } } ], @@ -290898,6 +291523,7 @@ "4": { "57": 2, "62": 1, + "113": 1, "152": 1, "160": 1, "258": 1, @@ -290997,7 +291623,7 @@ "104": 1, "105": 1, "112": 2, - "113": 1, + "113": 4, "115": 1, "116": 2, "118": 2, @@ -291038,6 +291664,7 @@ "218": 2, "221": 1, "226": 2, + "229": 1, "239": 1, "241": 1, "246": 1, @@ -291045,6 +291672,7 @@ "257": 1, "268": 1, "271": 3, + "272": 1, "274": 1, "275": 1 } @@ -291408,7 +292036,8 @@ "130": 2, "134": 1, "220": 1, - "225": 1 + "225": 1, + "272": 1 }, "4": { "0": 9, @@ -291448,16 +292077,18 @@ "226": 1, "227": 1, "228": 3, - "229": 6, + "229": 11, "230": 4, "231": 3, "233": 10, "240": 27, "244": 3, "246": 1, + "251": 2, "255": 1, "256": 1, "260": 4, + "272": 3, "274": 1 } } @@ -291514,6 +292145,14 @@ } } ], + [ + "mets", + { + "4": { + "112": 1 + } + } + ], [ "met", { @@ -292356,6 +292995,7 @@ "95": 1, "97": 2, "105": 1, + "113": 2, "114": 11, "118": 1, "119": 1, @@ -292968,6 +293608,7 @@ "224": 1, "232": 1, "240": 1, + "246": 1, "267": 2 } } @@ -293214,7 +293855,8 @@ "167": 3, "173": 1, "229": 3, - "231": 1 + "231": 1, + "251": 2 } } ], @@ -293360,6 +294002,7 @@ "240": 1, "244": 4, "250": 1, + "251": 2, "252": 1, "254": 5, "257": 5, @@ -293372,6 +294015,7 @@ "267": 1, "268": 2, "270": 1, + "272": 1, "273": 3 } } @@ -293583,7 +294227,7 @@ "239": 2, "240": 20, "244": 1, - "246": 1, + "246": 2, "247": 16, "248": 1, "250": 2, @@ -293744,7 +294388,8 @@ "261": 1, "264": 1, "267": 1, - "268": 1 + "268": 1, + "272": 1 }, "4": { "0": 49, @@ -293857,7 +294502,7 @@ "110": 5, "111": 6, "112": 2, - "113": 68, + "113": 73, "114": 23, "115": 8, "116": 13, @@ -293956,7 +294601,7 @@ "226": 60, "227": 41, "228": 9, - "229": 34, + "229": 35, "230": 52, "231": 22, "232": 49, @@ -293977,6 +294622,7 @@ "247": 21, "248": 2, "250": 13, + "251": 1, "252": 7, "253": 21, "254": 35, @@ -293996,6 +294642,7 @@ "268": 23, "269": 7, "270": 9, + "272": 3, "273": 7, "275": 7 } @@ -294412,7 +295059,9 @@ "skewed", { "4": { - "13": 1 + "13": 1, + "251": 1, + "272": 2 } } ], @@ -294567,6 +295216,7 @@ "53": 1, "96": 1, "98": 1, + "113": 1, "131": 1, "144": 1, "152": 1, @@ -294634,6 +295284,7 @@ "106": 2, "107": 1, "108": 1, + "113": 2, "115": 2, "118": 1, "119": 1, @@ -294983,7 +295634,7 @@ "226": 14, "227": 6, "228": 7, - "229": 23, + "229": 25, "230": 13, "231": 16, "232": 2, @@ -295000,7 +295651,7 @@ "243": 3, "244": 8, "245": 3, - "246": 6, + "246": 8, "247": 31, "248": 4, "249": 2, @@ -295024,7 +295675,7 @@ "269": 2, "270": 2, "271": 5, - "272": 1, + "272": 2, "273": 6, "274": 16, "275": 7 @@ -296042,6 +296693,7 @@ "106": 2, "154": 1, "155": 3, + "229": 1, "240": 2, "260": 3, "265": 2, @@ -296458,14 +297110,6 @@ } } ], - [ - "sys", - { - "4": { - "214": 4 - } - } - ], [ "syscalls", { @@ -296505,6 +297149,15 @@ } } ], + [ + "sys", + { + "4": { + "112": 2, + "214": 4 + } + } + ], [ "systolic", { @@ -296866,7 +297519,7 @@ "225": 3, "226": 1, "227": 4, - "229": 23, + "229": 35, "230": 9, "231": 7, "232": 14, @@ -297162,6 +297815,7 @@ "12": 1, "13": 10, "112": 1, + "229": 1, "262": 1 } } @@ -297205,6 +297859,7 @@ "51": 1, "73": 1, "87": 1, + "113": 1, "157": 5, "161": 2, "162": 6, @@ -297518,6 +298173,7 @@ "107": 1, "108": 6, "109": 1, + "113": 7, "115": 3, "116": 1, "117": 2, @@ -297818,6 +298474,7 @@ "23": 2, "24": 1, "63": 3, + "113": 3, "253": 8 } } @@ -298157,9 +298814,10 @@ "4": { "44": 1, "55": 1, - "113": 1, + "113": 3, "152": 1, - "239": 1 + "239": 1, + "272": 1 } } ], @@ -298189,7 +298847,7 @@ "100": 1, "104": 3, "105": 1, - "113": 2, + "113": 3, "144": 1, "152": 1, "161": 1, @@ -298361,8 +299019,8 @@ { "4": { "58": 1, - "112": 1, - "113": 7, + "112": 3, + "113": 9, "152": 14, "155": 5, "156": 3, @@ -298684,6 +299342,7 @@ "104": 4, "105": 1, "108": 1, + "113": 2, "120": 1, "121": 1, "123": 1, @@ -298789,8 +299448,8 @@ "104": 13, "105": 8, "108": 2, - "112": 10, - "113": 23, + "112": 9, + "113": 33, "114": 1, "118": 1, "119": 1, @@ -299082,8 +299741,8 @@ "109": 1, "110": 3, "111": 1, - "112": 3, - "113": 1, + "112": 5, + "113": 10, "114": 2, "115": 13, "116": 2, @@ -299168,7 +299827,7 @@ "226": 12, "227": 1, "228": 5, - "229": 12, + "229": 13, "230": 14, "231": 6, "232": 3, @@ -299184,10 +299843,10 @@ "242": 3, "244": 11, "245": 2, - "246": 12, + "246": 13, "249": 7, "250": 11, - "251": 9, + "251": 13, "252": 2, "254": 1, "255": 3, @@ -299195,7 +299854,7 @@ "257": 1, "258": 1, "259": 2, - "260": 13, + "260": 14, "261": 4, "262": 1, "263": 6, @@ -299205,6 +299864,7 @@ "269": 3, "270": 6, "271": 10, + "272": 1, "273": 2, "274": 6, "275": 7 @@ -299260,6 +299920,7 @@ "84": 1, "91": 1, "95": 1, + "113": 1, "115": 1, "117": 1, "119": 1, @@ -299286,6 +299947,7 @@ "239": 1, "241": 4, "246": 1, + "251": 1, "258": 2, "275": 1 } @@ -299428,6 +300090,7 @@ "107": 8, "108": 4, "109": 2, + "112": 1, "113": 7, "114": 1, "115": 2, @@ -299485,7 +300148,7 @@ "226": 24, "227": 17, "228": 1, - "229": 10, + "229": 11, "230": 12, "231": 4, "232": 17, @@ -299678,6 +300341,7 @@ "94": 2, "110": 1, "111": 1, + "113": 1, "117": 1, "119": 1, "127": 1, @@ -300077,7 +300741,7 @@ "20": 4, "93": 3, "104": 1, - "112": 6, + "112": 7, "141": 2, "147": 7, "244": 1 @@ -300435,6 +301099,7 @@ "74": 1, "75": 1, "97": 1, + "113": 1, "115": 1, "127": 1, "149": 1, @@ -300490,7 +301155,7 @@ "94": 1, "106": 1, "109": 1, - "113": 3, + "113": 4, "114": 16, "128": 1, "136": 2, @@ -301537,7 +302202,6 @@ "245": 3, "247": 1, "250": 7, - "251": 1, "253": 3, "254": 1, "258": 4, @@ -301688,11 +302352,12 @@ "shuffle", { "4": { - "112": 5, + "112": 6, "148": 1, "154": 4, "155": 2, - "156": 1 + "156": 1, + "229": 2 } } ], @@ -302148,6 +302813,7 @@ "47": 2, "89": 1, "95": 1, + "112": 1, "113": 1, "117": 1, "120": 1, @@ -302230,7 +302896,8 @@ "106": 1, "108": 2, "111": 1, - "113": 1, + "112": 1, + "113": 4, "115": 1, "117": 1, "119": 1, @@ -302336,7 +303003,7 @@ "105": 1, "110": 1, "111": 3, - "112": 2, + "112": 3, "115": 1, "117": 1, "120": 2, @@ -303713,7 +304380,7 @@ "98": 2, "107": 1, "112": 2, - "113": 4, + "113": 6, "116": 1, "134": 2, "146": 2, @@ -303785,6 +304452,7 @@ "92": 29, "93": 102, "103": 7, + "112": 2, "113": 8, "130": 2, "133": 1, @@ -304168,6 +304836,7 @@ "107": 10, "108": 1, "110": 1, + "113": 1, "114": 1, "115": 4, "117": 5, @@ -304237,7 +304906,7 @@ "226": 9, "227": 2, "228": 1, - "229": 10, + "229": 13, "230": 2, "231": 1, "232": 9, @@ -304249,7 +304918,7 @@ "239": 12, "240": 4, "244": 4, - "246": 2, + "246": 3, "251": 2, "253": 6, "256": 2, @@ -304439,7 +305108,7 @@ "225": 6, "226": 11, "227": 6, - "229": 7, + "229": 8, "230": 3, "231": 3, "232": 4, @@ -304669,6 +305338,7 @@ "242": 2, "243": 1, "244": 8, + "246": 1, "250": 1, "251": 1, "253": 3, @@ -305081,7 +305751,7 @@ "225": 2, "226": 14, "228": 1, - "229": 12, + "229": 17, "230": 71, "232": 2, "233": 1, @@ -305401,6 +306071,7 @@ "58": 1, "84": 1, "93": 4, + "113": 1, "118": 1, "124": 1, "144": 2, @@ -305414,7 +306085,7 @@ "209": 2, "214": 3, "215": 1, - "229": 2, + "229": 3, "244": 1, "250": 1, "251": 3 @@ -305454,6 +306125,8 @@ "84": 1, "94": 3, "97": 1, + "112": 1, + "113": 1, "115": 3, "119": 1, "120": 1, @@ -306133,8 +306806,8 @@ "109": 2, "110": 1, "111": 2, - "112": 2, - "113": 8, + "112": 4, + "113": 9, "115": 8, "117": 3, "118": 4, @@ -306238,7 +306911,7 @@ "226": 9, "227": 4, "228": 2, - "229": 7, + "229": 10, "230": 12, "231": 6, "232": 7, @@ -306255,11 +306928,11 @@ "243": 1, "244": 7, "245": 1, - "246": 11, + "246": 12, "248": 1, "249": 3, "250": 3, - "251": 5, + "251": 4, "252": 1, "253": 1, "254": 5, @@ -306267,7 +306940,7 @@ "257": 5, "258": 7, "259": 2, - "260": 2, + "260": 3, "261": 1, "263": 5, "266": 1, @@ -306667,6 +307340,7 @@ "57": 7, "93": 1, "101": 1, + "113": 1, "133": 1, "223": 1, "224": 3, @@ -306820,6 +307494,7 @@ "107": 1, "108": 1, "112": 1, + "113": 1, "114": 1, "116": 1, "118": 1, @@ -306895,6 +307570,7 @@ "40": 1, "48": 1, "152": 1, + "229": 1, "230": 1 } } @@ -308057,6 +308733,7 @@ "232": 1, "242": 1, "244": 2, + "246": 1, "261": 2, "275": 2 } @@ -308092,6 +308769,7 @@ "215": 1, "230": 1, "236": 1, + "251": 1, "255": 1 }, "3": { @@ -308206,7 +308884,7 @@ "110": 4, "111": 2, "112": 7, - "113": 1, + "113": 2, "114": 1, "115": 1, "117": 4, @@ -308329,12 +309007,12 @@ "243": 2, "244": 11, "245": 2, - "246": 10, + "246": 12, "247": 3, "248": 1, "249": 5, "250": 7, - "251": 2, + "251": 3, "252": 2, "253": 9, "254": 3, @@ -308663,6 +309341,7 @@ "8": 1, "57": 2, "58": 1, + "113": 1, "119": 1, "147": 1, "178": 1, @@ -308821,8 +309500,10 @@ "210": 1, "211": 1, "214": 1, + "251": 3, "270": 1, "271": 1, + "272": 2, "273": 1 } } @@ -308908,6 +309589,7 @@ "215": 1, "217": 1, "223": 5, + "229": 1, "246": 1, "254": 12, "263": 1, @@ -309191,7 +309873,7 @@ "102": 15, "103": 67, "112": 1, - "113": 1, + "113": 3, "114": 1, "127": 20, "128": 23, @@ -309548,6 +310230,7 @@ "244": 1, "245": 1, "248": 1, + "251": 1, "260": 1, "263": 1, "274": 1 @@ -309588,7 +310271,8 @@ "257": 1, "258": 2, "259": 1, - "260": 1 + "260": 1, + "272": 2 } } ], @@ -309605,7 +310289,9 @@ "59": 4, "94": 8, "95": 2, - "130": 5 + "130": 5, + "251": 1, + "272": 1 } } ], @@ -309716,7 +310402,7 @@ "262": 4, "266": 1, "271": 1, - "272": 3, + "272": 2, "274": 4 } } @@ -310338,6 +311024,7 @@ "98": 3, "106": 1, "110": 1, + "113": 2, "115": 1, "117": 4, "119": 1, @@ -310776,7 +311463,7 @@ "239": 1, "240": 1, "241": 1, - "246": 3, + "246": 4, "251": 1, "252": 2, "253": 1, @@ -311192,7 +311879,7 @@ "224": 2, "225": 3, "226": 4, - "229": 7, + "229": 9, "230": 4, "231": 2, "232": 1, @@ -311207,7 +311894,7 @@ "243": 3, "244": 18, "245": 3, - "246": 9, + "246": 12, "250": 1, "251": 5, "252": 2, @@ -311548,7 +312235,7 @@ "103": 1, "106": 3, "108": 2, - "112": 2, + "112": 3, "113": 16, "114": 18, "115": 14, @@ -312228,6 +312915,7 @@ "239": 3, "249": 1, "250": 4, + "260": 1, "269": 2 } } @@ -312455,6 +313143,7 @@ "163": 1, "176": 1, "209": 1, + "229": 1, "230": 1, "244": 1, "257": 1, @@ -312907,7 +313596,7 @@ "224": 5, "226": 13, "227": 17, - "229": 15, + "229": 17, "230": 105, "231": 59, "232": 9, @@ -312919,22 +313608,22 @@ "240": 1, "241": 1, "244": 1, - "246": 17, + "246": 18, "250": 5, - "251": 4, + "251": 5, "252": 17, "253": 6, "255": 6, "257": 1, "259": 43, - "260": 17, + "260": 19, "261": 13, "262": 8, "263": 1, "265": 1, "266": 2, "268": 3, - "272": 2, + "272": 3, "273": 1 } } @@ -313132,7 +313821,7 @@ "104": 1, "110": 2, "111": 2, - "113": 2, + "113": 3, "115": 5, "117": 5, "119": 2, @@ -313355,6 +314044,8 @@ "104": 2, "105": 5, "106": 1, + "112": 2, + "113": 1, "115": 1, "116": 2, "146": 6, @@ -313443,8 +314134,7 @@ "sims", { "4": { - "25": 1, - "251": 1 + "25": 1 } } ], @@ -313467,6 +314157,7 @@ "25": 18, "107": 1, "110": 1, + "112": 1, "115": 8, "116": 1, "117": 1, @@ -313474,14 +314165,15 @@ "192": 5, "216": 25, "226": 14, + "229": 2, "243": 1, "244": 9, "245": 1, - "246": 33, + "246": 44, "248": 5, "251": 10, "271": 1, - "272": 5 + "272": 6 } } ], @@ -313596,7 +314288,7 @@ "25": 1, "107": 1, "192": 1, - "272": 1 + "272": 2 }, "4": { "16": 3, @@ -313611,7 +314303,8 @@ "244": 1, "246": 6, "248": 1, - "251": 1, + "251": 2, + "260": 1, "268": 1, "271": 2, "272": 4, @@ -313642,7 +314335,7 @@ "244": 1, "248": 3, "251": 1, - "272": 3 + "272": 2 }, "4": { "0": 5, @@ -313674,7 +314367,7 @@ "107": 2, "108": 2, "110": 2, - "112": 1, + "112": 2, "113": 1, "115": 13, "116": 3, @@ -313717,7 +314410,7 @@ "225": 1, "226": 12, "228": 6, - "229": 9, + "229": 12, "230": 4, "231": 4, "232": 1, @@ -313729,11 +314422,11 @@ "243": 6, "244": 20, "245": 4, - "246": 30, + "246": 33, "247": 1, "248": 18, "250": 2, - "251": 10, + "251": 14, "255": 2, "256": 4, "258": 12, @@ -313744,7 +314437,7 @@ "268": 5, "270": 4, "271": 4, - "272": 8, + "272": 10, "273": 3, "275": 1 } @@ -313801,9 +314494,10 @@ "4": { "16": 1, "73": 1, + "229": 1, "244": 10, - "246": 35, - "251": 4 + "246": 43, + "251": 8 } } ], @@ -314829,7 +315523,7 @@ "221": 1, "223": 7, "227": 1, - "229": 26, + "229": 27, "230": 5, "231": 2, "232": 2, @@ -316247,9 +316941,9 @@ "240": 2, "241": 3, "244": 1, - "246": 1, + "246": 2, "252": 1, - "260": 79, + "260": 83, "261": 3, "264": 3, "266": 5, @@ -316340,6 +317034,7 @@ "94": 3, "104": 2, "108": 1, + "113": 1, "130": 8, "132": 1, "140": 2, @@ -316458,6 +317153,7 @@ "215": 4, "216": 1, "228": 1, + "229": 1, "243": 3, "244": 2, "245": 2, @@ -316501,15 +317197,16 @@ "216": 18, "226": 11, "228": 1, + "229": 1, "243": 5, "244": 2, "245": 8, - "246": 19, - "251": 4, + "246": 25, + "251": 6, "256": 2, "263": 5, "271": 1, - "272": 12, + "272": 13, "273": 3 } } @@ -316687,7 +317384,7 @@ "225": 3, "226": 9, "227": 3, - "229": 12, + "229": 14, "230": 18, "231": 13, "232": 16, @@ -316718,7 +317415,7 @@ "266": 10, "267": 1, "270": 2, - "272": 2 + "272": 3 } } ], @@ -317407,6 +318104,7 @@ "272": 1 }, "4": { + "112": 1, "214": 3, "250": 3, "272": 3 @@ -317923,7 +318621,7 @@ "244": 10, "247": 5, "250": 2, - "251": 1, + "251": 2, "253": 5, "256": 1, "258": 3, @@ -318180,7 +318878,7 @@ "225": 2, "226": 10, "228": 1, - "229": 8, + "229": 9, "230": 5, "231": 2, "232": 2, @@ -318194,10 +318892,10 @@ "242": 1, "244": 3, "245": 1, - "246": 3, + "246": 4, "249": 1, "250": 3, - "251": 3, + "251": 5, "253": 1, "254": 4, "256": 1, @@ -318214,6 +318912,7 @@ "269": 4, "270": 4, "271": 9, + "272": 3, "273": 2, "274": 3, "275": 3 @@ -319092,6 +319791,7 @@ "244": 6, "246": 3, "248": 2, + "251": 1, "254": 1, "258": 7, "260": 1, @@ -319640,7 +320340,7 @@ "166": 1, "187": 2, "227": 1, - "246": 2, + "246": 3, "254": 3, "262": 1 } @@ -319790,6 +320490,14 @@ } } ], + [ + "stringify", + { + "4": { + "113": 1 + } + } + ], [ "stringly", { @@ -319806,6 +320514,14 @@ } } ], + [ + "stringsdata", + { + "4": { + "113": 4 + } + } + ], [ "stringsdict", { @@ -319838,8 +320554,8 @@ "94": 1, "96": 1, "101": 1, - "112": 25, - "113": 62, + "112": 30, + "113": 67, "124": 2, "126": 1, "130": 2, @@ -320160,7 +320876,8 @@ "94": 1, "106": 1, "108": 2, - "113": 2, + "112": 1, + "113": 3, "114": 1, "115": 1, "141": 1, @@ -320403,7 +321120,7 @@ "226": 25, "227": 4, "228": 1, - "229": 9, + "229": 10, "230": 13, "231": 3, "232": 14, @@ -320527,8 +321244,8 @@ "108": 8, "109": 6, "110": 2, - "112": 21, - "113": 147, + "112": 22, + "113": 155, "114": 58, "115": 8, "116": 29, @@ -321315,6 +322032,14 @@ } } ], + [ + "staled", + { + "4": { + "113": 1 + } + } + ], [ "stale", { @@ -321355,7 +322080,7 @@ "109": 7, "110": 8, "112": 3, - "113": 6, + "113": 10, "115": 1, "116": 2, "117": 5, @@ -321403,7 +322128,8 @@ { "4": { "68": 1, - "152": 1 + "152": 1, + "229": 1 } } ], @@ -321547,7 +322273,7 @@ "206": 1, "221": 1, "223": 1, - "229": 9, + "229": 10, "233": 2, "235": 2, "240": 1, @@ -321721,7 +322447,7 @@ "107": 5, "109": 1, "110": 1, - "113": 3, + "113": 4, "114": 3, "117": 3, "120": 2, @@ -321988,7 +322714,7 @@ "218": 1, "221": 1, "228": 1, - "229": 9, + "229": 10, "230": 1, "233": 2, "236": 3, @@ -322416,6 +323142,7 @@ "220": 2, "223": 1, "226": 2, + "229": 1, "236": 5, "237": 5, "246": 2, @@ -322546,7 +323273,7 @@ "109": 25, "110": 22, "112": 6, - "113": 18, + "113": 21, "114": 1, "115": 6, "116": 13, @@ -322635,7 +323362,7 @@ "226": 37, "227": 61, "228": 3, - "229": 3, + "229": 6, "230": 11, "231": 19, "232": 35, @@ -322655,7 +323382,7 @@ "247": 1, "248": 1, "250": 1, - "251": 6, + "251": 7, "254": 6, "255": 3, "256": 1, @@ -322674,7 +323401,7 @@ "269": 1, "270": 15, "271": 23, - "272": 5, + "272": 4, "273": 8, "274": 3, "275": 4 @@ -323342,7 +324069,7 @@ "225": 5, "226": 25, "228": 1, - "229": 9, + "229": 19, "230": 52, "231": 20, "232": 73, @@ -323416,7 +324143,8 @@ "styles", { "1": { - "63": 1 + "63": 1, + "251": 1 }, "3": { "63": 1, @@ -323438,7 +324166,7 @@ "93": 1, "101": 1, "106": 6, - "113": 1, + "113": 2, "120": 1, "128": 1, "129": 1, @@ -323453,8 +324181,10 @@ "180": 2, "221": 5, "224": 1, + "229": 1, "233": 1, "241": 1, + "251": 3, "257": 1, "266": 4, "268": 1 @@ -323569,7 +324299,7 @@ "223": 9, "224": 4, "227": 5, - "229": 4, + "229": 8, "230": 1, "231": 1, "232": 1, @@ -323582,6 +324312,7 @@ "244": 1, "246": 1, "248": 1, + "251": 8, "255": 4, "257": 7, "259": 4, @@ -323590,6 +324321,7 @@ "265": 2, "266": 9, "268": 3, + "272": 22, "275": 1 } } @@ -323611,6 +324343,7 @@ "221": 3, "222": 1, "227": 2, + "229": 2, "240": 5, "257": 3, "259": 1, @@ -324612,7 +325345,9 @@ "174": 1, "201": 1, "204": 2, - "209": 1 + "209": 1, + "251": 1, + "272": 2 } } ], @@ -324739,11 +325474,13 @@ "219": 2, "223": 2, "226": 4, + "229": 1, "242": 2, "243": 1, "244": 3, - "246": 3, + "246": 4, "250": 4, + "251": 1, "254": 2, "270": 1, "273": 11, @@ -325261,7 +325998,8 @@ { "4": { "11": 1, - "38": 1 + "38": 1, + "113": 1 } } ], @@ -325296,6 +326034,7 @@ "203": 3, "220": 1, "227": 1, + "229": 1, "231": 5, "244": 1, "246": 2, @@ -325498,6 +326237,9 @@ [ "supplied", { + "1": { + "251": 1 + }, "4": { "13": 1, "33": 1, @@ -325514,7 +326256,8 @@ "202": 1, "226": 1, "236": 1, - "250": 1 + "250": 1, + "251": 1 } } ], @@ -325918,7 +326661,7 @@ "104": 1, "106": 5, "108": 2, - "112": 15, + "112": 16, "113": 8, "114": 3, "116": 1, @@ -327090,13 +327833,15 @@ "44": 1, "46": 1, "92": 1, + "113": 1, "170": 1, "174": 1, "178": 1, "186": 1, "220": 1, "230": 1, - "237": 1 + "237": 1, + "246": 1 } } ], @@ -329468,6 +330213,7 @@ "240": 3, "242": 2, "243": 1, + "251": 1, "253": 5, "254": 2, "255": 1, @@ -329481,7 +330227,7 @@ "266": 1, "268": 1, "270": 5, - "272": 2 + "272": 3 } } ], @@ -329582,6 +330328,7 @@ "243": 1, "244": 4, "245": 3, + "251": 1, "252": 3, "253": 4, "259": 5, @@ -330047,6 +330794,7 @@ "223": 1, "224": 1, "227": 1, + "229": 1, "230": 3, "232": 2, "233": 1, @@ -331922,7 +332670,7 @@ "224": 16, "226": 3, "227": 9, - "229": 13, + "229": 17, "230": 12, "231": 3, "233": 6, @@ -332332,6 +333080,7 @@ "213": 1, "214": 1, "217": 1, + "229": 1, "230": 1, "233": 2, "244": 1, @@ -332543,7 +333292,7 @@ "257": 2, "258": 1, "259": 2, - "260": 1, + "260": 2, "261": 5, "262": 2, "263": 2, @@ -333104,7 +333853,8 @@ "165": 1, "188": 1, "190": 1, - "214": 6 + "214": 6, + "229": 1 } } ], @@ -333255,7 +334005,7 @@ "217": 1, "224": 1, "225": 1, - "229": 4, + "229": 5, "239": 1, "260": 1, "273": 2 @@ -333298,8 +334048,8 @@ "200": 5, "215": 2, "216": 1, - "229": 8, - "246": 1, + "229": 17, + "246": 5, "263": 1, "273": 1, "274": 1, @@ -334035,7 +334785,7 @@ "225": 1, "226": 5, "228": 1, - "229": 9, + "229": 10, "230": 20, "231": 26, "232": 9, @@ -334467,7 +335217,7 @@ "223": 2, "225": 1, "226": 1, - "229": 1, + "229": 3, "232": 3, "235": 1, "236": 1, @@ -334800,8 +335550,10 @@ "224": 1, "227": 1, "231": 1, + "251": 1, "253": 1, - "259": 1 + "259": 1, + "272": 1 } } ], @@ -335846,7 +336598,7 @@ "226": 4, "227": 2, "228": 1, - "229": 7, + "229": 8, "230": 1, "232": 24, "233": 1, @@ -335878,7 +336630,7 @@ "269": 2, "270": 4, "271": 1, - "272": 3, + "272": 4, "273": 1, "274": 5, "275": 7 @@ -336338,6 +337090,7 @@ "217": 1, "220": 1, "238": 1, + "246": 1, "250": 1, "264": 2 } @@ -336447,6 +337200,7 @@ "269": 1, "270": 1, "271": 1, + "272": 1, "273": 1, "275": 1 } @@ -337373,7 +338127,7 @@ "225": 27, "226": 34, "228": 1, - "229": 2, + "229": 3, "230": 1, "231": 3, "232": 11, @@ -337388,6 +338142,7 @@ "245": 1, "246": 5, "249": 1, + "251": 1, "252": 3, "253": 5, "254": 11, @@ -337396,12 +338151,12 @@ "257": 6, "258": 1, "259": 9, - "260": 3, + "260": 4, "262": 1, "264": 1, "267": 2, "269": 1, - "272": 2, + "272": 4, "273": 5, "275": 4 } @@ -337499,7 +338254,7 @@ "110": 3, "111": 1, "112": 2, - "113": 1, + "113": 3, "114": 1, "115": 6, "117": 3, @@ -337579,7 +338334,7 @@ "226": 8, "227": 3, "228": 3, - "229": 9, + "229": 10, "230": 20, "231": 11, "232": 31, @@ -337592,7 +338347,7 @@ "240": 2, "244": 3, "245": 1, - "246": 3, + "246": 4, "247": 1, "249": 2, "252": 2, @@ -337687,6 +338442,7 @@ "194": 1, "210": 1, "211": 1, + "246": 2, "263": 1, "270": 3 } @@ -337885,7 +338641,7 @@ "210": 2, "211": 1, "221": 6, - "229": 31, + "229": 35, "235": 2, "237": 2, "246": 1, @@ -337933,6 +338689,7 @@ "1": 1, "13": 1, "45": 1, + "113": 1, "120": 1, "152": 2, "158": 1, @@ -338122,7 +338879,8 @@ "244": 3, "246": 1, "249": 1, - "261": 1 + "261": 1, + "272": 1 } } ], @@ -338540,7 +339298,7 @@ "248": 1, "249": 2, "250": 3, - "251": 1, + "251": 2, "252": 1, "253": 1, "254": 4, @@ -338559,6 +339317,7 @@ "268": 4, "270": 2, "271": 3, + "272": 2, "273": 5 } } @@ -339574,11 +340333,11 @@ "254": 2, "257": 2, "259": 1, - "260": 1, + "260": 2, "263": 1, "264": 2, "270": 1, - "272": 2, + "272": 3, "274": 2, "275": 3 } @@ -339777,7 +340536,7 @@ "246": 1, "247": 1, "250": 2, - "251": 3, + "251": 4, "254": 3, "255": 2, "256": 5, @@ -340075,7 +340834,7 @@ "109": 1, "110": 2, "112": 5, - "113": 41, + "113": 52, "148": 1, "152": 28, "153": 2, @@ -340917,6 +341676,7 @@ "85": 1, "87": 1, "112": 4, + "113": 1, "119": 1, "130": 1, "136": 1, @@ -341183,11 +341943,11 @@ "241": 16, "242": 3, "244": 8, - "246": 3, + "246": 4, "247": 2, "249": 6, "250": 5, - "251": 3, + "251": 4, "253": 6, "254": 2, "255": 1, @@ -341774,7 +342534,7 @@ "105": 1, "111": 1, "112": 1, - "113": 1, + "113": 2, "114": 1, "120": 4, "123": 1, @@ -342365,6 +343125,7 @@ "131": 2, "221": 5, "222": 1, + "229": 1, "266": 2 } } @@ -342509,6 +343270,7 @@ "239": 1, "241": 1, "243": 1, + "246": 1, "247": 1, "248": 2, "249": 1, @@ -342655,10 +343417,10 @@ "219": 1, "222": 1, "226": 2, - "229": 11, + "229": 13, "244": 6, "245": 7, - "246": 14, + "246": 19, "248": 9, "249": 3, "250": 1, @@ -344935,7 +345697,7 @@ "109": 1, "110": 1, "111": 1, - "113": 17, + "113": 20, "114": 2, "115": 16, "117": 1, @@ -345057,7 +345819,7 @@ "256": 1, "257": 7, "258": 3, - "260": 2, + "260": 3, "261": 6, "262": 4, "263": 4, @@ -345190,7 +345952,7 @@ "222": 2, "223": 1, "226": 1, - "229": 2, + "229": 4, "235": 4, "237": 1, "244": 3, @@ -345358,14 +346120,6 @@ } } ], - [ - "colon", - { - "4": { - "250": 1 - } - } - ], [ "colocated", { @@ -345374,6 +346128,15 @@ } } ], + [ + "colon", + { + "4": { + "113": 1, + "250": 1 + } + } + ], [ "colorful", { @@ -345913,7 +346676,7 @@ "221": 1, "224": 4, "225": 1, - "229": 2, + "229": 3, "233": 2, "238": 1, "239": 8, @@ -347902,6 +348665,7 @@ "85": 1, "92": 2, "93": 1, + "113": 1, "128": 1, "172": 1, "200": 1, @@ -349222,6 +349986,7 @@ "241": 7, "242": 12, "246": 1, + "251": 1, "260": 1, "261": 1, "262": 2, @@ -349229,6 +349994,7 @@ "267": 1, "268": 1, "271": 3, + "272": 1, "273": 1, "274": 2, "275": 5 @@ -350953,7 +351719,7 @@ "217": 4, "221": 5, "224": 4, - "229": 4, + "229": 5, "230": 9, "231": 2, "233": 1, @@ -351051,7 +351817,7 @@ "225": 5, "226": 4, "227": 3, - "229": 3, + "229": 9, "230": 20, "231": 5, "232": 8, @@ -351590,7 +352356,7 @@ "225": 3, "226": 13, "227": 5, - "229": 20, + "229": 23, "230": 53, "231": 16, "232": 16, @@ -351606,6 +352372,7 @@ "243": 3, "244": 4, "245": 1, + "246": 1, "248": 3, "250": 4, "251": 2, @@ -351688,7 +352455,8 @@ "134": 1, "217": 1, "259": 1, - "267": 1 + "267": 1, + "272": 1 }, "4": { "0": 2, @@ -351769,7 +352537,7 @@ "108": 4, "109": 8, "112": 5, - "113": 8, + "113": 9, "114": 1, "115": 4, "117": 7, @@ -351834,7 +352602,7 @@ "225": 1, "227": 4, "228": 1, - "229": 1, + "229": 4, "231": 1, "232": 3, "233": 12, @@ -351860,6 +352628,7 @@ "268": 4, "269": 26, "270": 15, + "272": 1, "273": 10, "274": 1, "275": 3 @@ -352297,7 +353066,7 @@ "220": 11, "221": 11, "227": 3, - "229": 17, + "229": 18, "230": 1, "231": 1, "233": 5, @@ -352305,7 +353074,7 @@ "237": 1, "240": 3, "243": 1, - "246": 1, + "246": 2, "247": 1, "252": 1, "256": 2, @@ -352318,7 +353087,7 @@ "267": 2, "268": 7, "269": 8, - "272": 2, + "272": 3, "273": 1 } } @@ -352476,7 +353245,7 @@ "226": 2, "227": 1, "228": 5, - "229": 3, + "229": 4, "230": 6, "231": 1, "232": 3, @@ -352492,7 +353261,7 @@ "244": 8, "245": 1, "246": 3, - "251": 2, + "251": 3, "255": 2, "257": 8, "259": 3, @@ -352505,7 +353274,7 @@ "268": 4, "269": 12, "271": 3, - "272": 3, + "272": 4, "273": 3, "275": 1 } @@ -352704,6 +353473,7 @@ "226": 2, "229": 3, "230": 1, + "246": 3, "261": 2, "263": 2 } @@ -352730,7 +353500,7 @@ "226": 1, "227": 2, "229": 4, - "246": 9, + "246": 11, "251": 1, "257": 1, "261": 5, @@ -353150,7 +353920,6 @@ "269": 1, "270": 1, "271": 1, - "272": 1, "273": 1, "275": 3 } @@ -354027,7 +354796,7 @@ "108": 8, "111": 5, "112": 15, - "113": 25, + "113": 33, "114": 14, "115": 11, "116": 8, @@ -354178,6 +354947,7 @@ "246": 2, "249": 3, "250": 1, + "251": 1, "254": 1, "262": 1, "264": 1, @@ -354233,7 +355003,6 @@ "229": 2, "230": 2, "233": 2, - "251": 1, "258": 1, "263": 1, "274": 1 @@ -355782,6 +356551,7 @@ "98": 3, "104": 1, "112": 1, + "113": 1, "152": 1, "166": 3, "183": 1, @@ -355893,7 +356663,7 @@ "104": 2, "105": 2, "112": 1, - "113": 1, + "113": 2, "127": 1, "130": 2, "132": 1, @@ -355922,7 +356692,7 @@ "225": 3, "226": 1, "239": 1, - "246": 1, + "246": 2, "249": 12, "251": 1, "259": 1, @@ -356386,11 +357156,13 @@ "239": 1, "244": 3, "245": 2, + "251": 1, "254": 2, "255": 1, "262": 1, "264": 7, "270": 4, + "272": 2, "273": 1 } } @@ -356883,7 +357655,7 @@ "69": 1, "71": 1, "112": 8, - "113": 9, + "113": 11, "118": 2, "119": 1, "158": 2, @@ -357230,17 +358002,18 @@ "226": 3, "227": 1, "228": 2, + "229": 1, "230": 2, "237": 1, "239": 2, "243": 2, "244": 3, - "246": 6, + "246": 7, "247": 1, "248": 3, "249": 4, "250": 2, - "251": 2, + "251": 4, "260": 6, "271": 3, "273": 9, @@ -357593,7 +358366,7 @@ "108": 2, "110": 1, "111": 1, - "113": 1, + "113": 2, "114": 4, "115": 3, "116": 1, @@ -357895,7 +358668,7 @@ "101": 2, "106": 4, "112": 2, - "113": 4, + "113": 5, "144": 1, "145": 1, "146": 1, @@ -358357,7 +359130,7 @@ "atoire", { "4": { - "112": 1 + "112": 2 } } ], @@ -360850,7 +361623,7 @@ "226": 14, "227": 3, "228": 2, - "229": 1, + "229": 2, "230": 4, "231": 1, "232": 6, @@ -360868,7 +361641,7 @@ "248": 3, "249": 2, "250": 2, - "251": 1, + "251": 2, "253": 6, "254": 8, "255": 1, @@ -361830,6 +362603,23 @@ } } ], + [ + "ax5", + { + "4": { + "236": 2, + "237": 1 + } + } + ], + [ + "ax1", + { + "4": { + "236": 2 + } + } + ], [ "axe", { @@ -361840,9 +362630,10 @@ "251": 1 }, "4": { + "229": 1, "246": 3, - "251": 14, - "272": 78, + "251": 16, + "272": 77, "273": 1 } } @@ -361862,23 +362653,6 @@ } } ], - [ - "ax5", - { - "4": { - "236": 2, - "237": 1 - } - } - ], - [ - "ax1", - { - "4": { - "236": 2 - } - } - ], [ "ax", { @@ -361932,7 +362706,7 @@ "151": 1, "187": 2, "221": 12, - "229": 17, + "229": 18, "230": 2, "235": 2, "239": 2, @@ -362429,7 +363203,7 @@ "226": 9, "227": 6, "228": 4, - "229": 10, + "229": 12, "230": 7, "231": 6, "232": 1, @@ -362441,7 +363215,7 @@ "240": 18, "241": 3, "244": 9, - "246": 15, + "246": 17, "247": 62, "248": 13, "249": 7, @@ -362463,7 +363237,7 @@ "269": 5, "270": 3, "271": 6, - "272": 1, + "272": 2, "273": 5, "274": 4, "275": 7 @@ -362954,7 +363728,7 @@ "106": 2, "108": 1, "111": 1, - "113": 3, + "113": 4, "119": 1, "120": 4, "121": 1, @@ -363005,7 +363779,7 @@ "225": 1, "226": 5, "228": 1, - "229": 1, + "229": 3, "230": 9, "231": 2, "232": 1, @@ -363015,6 +363789,7 @@ "239": 1, "240": 5, "244": 4, + "246": 1, "247": 1, "248": 1, "249": 1, @@ -363135,6 +363910,7 @@ "247": 1, "249": 1, "250": 1, + "251": 1, "257": 1, "263": 2, "264": 2, @@ -363623,6 +364399,7 @@ "156": 182, "161": 2, "162": 1, + "229": 1, "231": 1, "263": 1 } @@ -364164,7 +364941,7 @@ "221": 16, "226": 16, "227": 2, - "229": 11, + "229": 16, "230": 61, "232": 1, "233": 2, @@ -365323,7 +366100,7 @@ "243": 5, "244": 14, "245": 4, - "246": 1, + "246": 2, "247": 1, "251": 1, "272": 4 @@ -365490,6 +366267,7 @@ "239": 2, "240": 5, "247": 1, + "251": 5, "252": 1, "253": 1, "257": 3, @@ -365497,7 +366275,8 @@ "260": 8, "261": 1, "262": 1, - "263": 4 + "263": 4, + "272": 1 } } ], @@ -368063,6 +368842,7 @@ "225": 4, "226": 2, "227": 1, + "229": 2, "230": 1, "231": 1, "232": 4, @@ -368142,6 +368922,7 @@ "217": 1, "225": 1, "226": 1, + "229": 1, "230": 1, "232": 1, "239": 2, @@ -368272,6 +369053,7 @@ "61": 2, "62": 1, "70": 1, + "112": 2, "132": 3, "136": 3, "137": 4, @@ -368790,8 +369572,8 @@ "109": 2, "110": 7, "111": 7, - "112": 33, - "113": 7, + "112": 36, + "113": 8, "114": 7, "115": 18, "116": 6, @@ -369177,7 +369959,7 @@ "109": 1, "110": 12, "111": 38, - "112": 10, + "112": 15, "113": 30, "114": 48, "115": 34, @@ -369302,7 +370084,7 @@ "243": 63, "244": 155, "245": 50, - "246": 27, + "246": 29, "247": 5, "248": 44, "249": 7, @@ -369532,7 +370314,7 @@ "108": 1, "109": 1, "111": 2, - "112": 3, + "112": 5, "113": 4, "114": 6, "115": 4, @@ -370288,6 +371070,7 @@ "225": 1, "226": 4, "227": 1, + "229": 1, "230": 1, "233": 1, "257": 1, @@ -370517,7 +371300,8 @@ "221": 2, "222": 3, "227": 9, - "229": 8, + "229": 9, + "246": 2, "253": 1, "255": 3, "256": 11, @@ -370648,7 +371432,7 @@ "225": 1, "226": 3, "227": 2, - "229": 3, + "229": 4, "230": 1, "232": 2, "246": 2, @@ -371192,6 +371976,7 @@ "220": 2, "223": 1, "226": 2, + "229": 1, "235": 1, "238": 1, "259": 3 @@ -371408,6 +372193,7 @@ "231": 2, "234": 1, "237": 3, + "246": 1, "249": 1, "250": 2, "254": 2, @@ -371415,6 +372201,7 @@ "266": 1, "270": 1, "271": 6, + "272": 1, "274": 3, "275": 1 } @@ -371603,6 +372390,7 @@ "108": 3, "111": 4, "112": 2, + "113": 1, "114": 2, "115": 4, "116": 1, @@ -371703,7 +372491,7 @@ "224": 6, "225": 3, "226": 2, - "229": 2, + "229": 4, "230": 22, "231": 13, "232": 8, @@ -371721,7 +372509,7 @@ "248": 2, "249": 2, "250": 10, - "251": 4, + "251": 5, "252": 2, "253": 1, "254": 19, @@ -373105,7 +373893,7 @@ "244": 37, "245": 11, "246": 6, - "251": 12, + "251": 13, "254": 1 } } @@ -373904,7 +374692,7 @@ "184": 2, "210": 2, "211": 2, - "246": 2, + "246": 9, "255": 1, "259": 2 } @@ -374123,7 +374911,8 @@ "52": 1, "73": 1, "211": 1, - "229": 1 + "229": 2, + "246": 1 } } ], @@ -374289,7 +375078,7 @@ { "4": { "8": 1, - "112": 1 + "112": 2 } } ], @@ -374331,6 +375120,7 @@ "84": 1, "89": 1, "112": 1, + "113": 1, "127": 1, "142": 1, "144": 1, @@ -374442,10 +375232,11 @@ "77": 1, "147": 1, "148": 1, - "229": 4, + "229": 7, "230": 2, "236": 1, "240": 1, + "246": 1, "251": 1, "255": 1, "257": 1 @@ -374529,6 +375320,7 @@ "103": 2, "106": 2, "108": 5, + "113": 2, "115": 3, "116": 1, "117": 1, @@ -374690,6 +375482,7 @@ "100": 1, "103": 2, "106": 3, + "113": 1, "114": 1, "115": 1, "117": 2, @@ -374763,7 +375556,7 @@ "238": 1, "240": 1, "245": 1, - "246": 5, + "246": 6, "248": 1, "251": 1, "252": 2, @@ -375115,6 +375908,7 @@ "249": 8, "251": 4, "256": 2, + "260": 1, "264": 1, "271": 1, "273": 1, @@ -375320,7 +376114,7 @@ "110": 2, "111": 3, "112": 5, - "113": 5, + "113": 6, "114": 4, "115": 7, "116": 1, @@ -375428,7 +376222,7 @@ "226": 4, "227": 2, "228": 1, - "229": 2, + "229": 3, "230": 14, "231": 1, "232": 13, @@ -375468,7 +376262,7 @@ "269": 4, "270": 5, "271": 4, - "272": 1, + "272": 2, "273": 12, "274": 7, "275": 10 @@ -375575,7 +376369,7 @@ "108": 4, "109": 3, "110": 4, - "113": 3, + "113": 4, "114": 4, "115": 3, "116": 2, @@ -375674,7 +376468,7 @@ "243": 4, "244": 2, "245": 5, - "246": 3, + "246": 4, "247": 1, "248": 3, "249": 2, @@ -375686,7 +376480,7 @@ "257": 5, "258": 4, "259": 1, - "260": 2, + "260": 3, "261": 3, "264": 3, "265": 2, @@ -375695,7 +376489,7 @@ "268": 2, "270": 3, "271": 1, - "272": 2, + "272": 3, "273": 2, "274": 8, "275": 5 @@ -376896,7 +377690,7 @@ "105": 1, "107": 2, "112": 2, - "113": 9, + "113": 10, "117": 1, "118": 2, "119": 3, @@ -376940,6 +377734,7 @@ "232": 2, "234": 1, "244": 2, + "251": 1, "253": 2, "255": 1, "257": 1, @@ -376995,6 +377790,7 @@ "55": 1, "66": 1, "90": 1, + "113": 1, "163": 1, "247": 1, "260": 2 @@ -377237,6 +378033,7 @@ "265": 1, "266": 1, "271": 1, + "272": 1, "273": 1, "275": 1 } @@ -377459,7 +378256,7 @@ "226": 14, "227": 5, "228": 8, - "229": 3, + "229": 4, "230": 4, "231": 4, "232": 13, @@ -377477,6 +378274,7 @@ "245": 3, "247": 3, "250": 1, + "251": 2, "253": 4, "254": 1, "255": 4, @@ -377496,7 +378294,7 @@ "269": 2, "270": 1, "271": 1, - "272": 1, + "272": 2, "273": 9, "274": 12, "275": 1 @@ -377773,7 +378571,7 @@ "104": 2, "105": 1, "112": 6, - "113": 3, + "113": 5, "114": 2, "120": 2, "121": 1, @@ -377913,6 +378711,7 @@ "97": 1, "106": 3, "107": 3, + "113": 1, "114": 1, "124": 1, "125": 1, @@ -378671,7 +379470,7 @@ "236": 2, "240": 2, "244": 1, - "246": 10, + "246": 11, "251": 1, "253": 42, "255": 2, @@ -378749,7 +379548,9 @@ "221": 1, "233": 1, "238": 1, - "270": 6 + "251": 1, + "270": 6, + "272": 2 } } ], @@ -378857,8 +379658,10 @@ "232": 6, "233": 12, "238": 9, + "251": 1, "260": 6, "270": 15, + "272": 1, "273": 1 } } @@ -378996,7 +379799,7 @@ "224": 16, "225": 1, "227": 2, - "229": 4, + "229": 5, "231": 1, "233": 1, "240": 12, @@ -379142,7 +379945,7 @@ "224": 2, "225": 3, "227": 6, - "229": 11, + "229": 12, "230": 9, "231": 2, "232": 4, @@ -380009,13 +380812,13 @@ "245": 8, "246": 3, "247": 8, - "251": 14, + "251": 17, "253": 1, "257": 1, "260": 1, "265": 3, "266": 3, - "272": 16, + "272": 19, "274": 1 } } @@ -380473,8 +381276,8 @@ "210": 1, "211": 2, "241": 1, - "250": 2, - "251": 1 + "246": 2, + "250": 2 } } ], @@ -534119,55 +534922,148 @@ "transla", "technically", "correct", + "own", + "disk", + "most", + "exact", + "installed", + "simulator", + "runtime", + "ships", + "shipped", + "strings", + "bundled", + "apps", + "same", + "text", + "read", + "mail", + "settings", + "there", + "binary", + "plists", + "so", + "them", + "plutil", + "bash", + "rt", + "xcrun", + "simctl", + "list", + "runtimes", + "python3", + "import", + "json", + "sys", + "print", + "root", + "load", + "stdin", + "os", + "ls", + "applications", + "app", + "grep", + "lproj", + "56", + "locales", + "27", + "fr", + "localizable", + "songs", + "keys", + "english", + "key", + "value", + "morceaux", + "french", + "korean", + "al", + "atoire", + "some", + "ship", + "no", + "en", + "reason", + "holds", + "hundreds", + "shortcuts", + "files", + "753", + "which", + "registered", + "siri", + "phrases", + "precedent", + "follow", + "phrase", + "structure", + "not", + "just", + "vocabulary", + "find", + "name", + "head", + "use", + "first", + "when", + "need", + "form", + "ui", + "str", + "support", + "pages", + "below", + "user", + "says", + "out", + "loud", + "different", + "register", + "button", + "may", + "lire", + "while", + "verb", + "mets", "primary", "sanity", "check", "authoritative", - "support", "multi", "locale", - "pages", - "source", + "spoken", + "fac", + "phras", "help", "articles", - "ship", "supported", "hand", "translated", "fetch", - "same", "article", "across", - "locales", "compare", "url", "pattern", - "english", "us", "https", "com", - "en", "id", - "french", - "fr", "japanese", "ja", "jp", - "korean", "ko", "kr", "german", "de", "workflow", - "find", "relevant", "system", - "app", "re", "mirror", "repeat", - "songs", - "load", + "source", "confirm", "covers", "term", @@ -534184,10 +535080,8 @@ "project", "glossary", "see", - "use", "web", "content", - "text", "only", "renders", "cleanly", @@ -534199,15 +535093,11 @@ "built", "queryable", "database", - "strings", "extracted", "xcstrings", - "files", - "os", "mac", "frameworks", "includ", - "ui", "framework", "kit", "faster", @@ -534215,8 +535105,6 @@ "hopp", "don", "appear", - "when", - "need", "10", "30", "terms", @@ -534225,8 +535113,6 @@ "pass", "look", "level", - "str", - "button", "label", "menu", "item", @@ -534248,23 +535134,19 @@ "review", "defensible", "returns", - "no", "hits", "stale", "data", "fram", "treat", "tool", - "not", "truth", "scraped", "published", "cross", "makes", "worth", - "first", "media", - "apps", "these", "15", "drive", @@ -534280,7 +535162,6 @@ "listen", "radio", "generic", - "settings", "preferences", "done", "cancel", @@ -535411,6 +536292,228 @@ "con", "nosotros", "version", + "two", + "styles", + "what", + "tool", + "must", + "read", + "either", + "itself", + "symbolic", + "first", + "below", + "second", + "uses", + "carries", + "explicit", + "songs", + "quality", + "high", + "playback", + "256", + "kbps", + "extracted", + "produces", + "so", + "present", + "else", + "handle", + "both", + "not", + "pick", + "one", + "top", + "level", + "field", + "always", + "whose", + "german", + "lands", + "under", + "de", + "hardcodes", + "reads", + "only", + "skips", + "every", + "search", + "over", + "keys", + "alone", + "misses", + "writ", + "byte", + "layout", + "generic", + "writer", + "rewrites", + "whole", + "measured", + "real", + "970", + "dump", + "indent", + "produced", + "26", + "531", + "line", + "diff", + "buried", + "actual", + "rule", + "spaces", + "separator", + "space", + "before", + "colon", + "encod", + "literal", + "utf", + "never", + "escapes", + "trail", + "newline", + "none", + "empty", + "blank", + "then", + "own", + "inside", + "sorted", + "point", + "map", + "writers", + "orders", + "see", + "apple", + "order", + "differently", + "xcstringstool", + "sorts", + "editor", + "matches", + "founda", + "standard", + "compare", + "which", + "punctua", + "aware", + "across", + "written", + "revisions", + "same", + "086", + "447", + "comparison", + "had", + "violations", + "while", + "plain", + "139", + "212", + "runn", + "sync", + "maintained", + "re", + "16", + "005", + "mean", + "did", + "change", + "match", + "whichever", + "already", + "ships", + "headless", + "path", + "out", + "run", + "through", + "xcrun", + "bash", + "modern", + "tmp", + "sd", + "find", + "sources", + "stringsdata", + "skip", + "mark", + "stale", + "print", + "substitute", + "mac", + "os", + "no", + "globstar", + "silently", + "directory", + "prunes", + "additive", + "merge", + "marked", + "translations", + "deleted", + "outright", + "exit", + "warn", + "untranslated", + "vanished", + "came", + "back", + "guard", + "synced", + "intact", + "122", + "entries", + "all", + "survived", + "makes", + "filename", + "matter", + "twice", + "takes", + "noth", + "added", + "without", + "flag", + "pruned", + "staled", + "emits", + "canonical", + "when", + "someth", + "write", + "pure", + "reformat", + "left", + "exactly", + "repair", + "bad", + "does", + "do", + "parse", + "mutate", + "touch", + "emit", + "way", + "them", + "java", + "script", + "also", + "hoists", + "integer", + "like", + "30", + "front", + "stringify", + "round", + "trip", + "reorders", + "into", + "preserv", + "instead", "states", "tracks", "hasn", @@ -535424,13 +536527,9 @@ "reviewed", "approved", "current", - "stale", - "no", "longer", "found", - "itself", "word", - "not", "writes", "workflow", "developer", @@ -536366,7 +537465,64 @@ "acknowledge", "matters", "accurate", - "transla" + "transla", + "expect", + "separate", + "two", + "meanings", + "gives", + "does", + "split", + "english", + "used", + "concepts", + "merged", + "high", + "energy", + "level", + "track", + "playback", + "quality", + "256", + "kbps", + "json", + "xcstringstool", + "sync", + "says", + "so", + "notice", + "multiple", + "then", + "whichever", + "picked", + "both", + "give", + "each", + "own", + "keep", + "default", + "value", + "bites", + "hardest", + "where", + "reused", + "across", + "features", + "picker", + "label", + "siri", + "phrase", + "say", + "because", + "sense", + "shows", + "up", + "feature", + "renam", + "any", + "enum", + "search", + "exact" ], "Troubleshooting": [ "troubleshoot", @@ -701994,19 +703150,84 @@ "keyboard", "accessory", "portrait", + "host", + "container", + "join", + "inside", + "containers", + "same", + "bottom", + "none", + "them", + "bare", + "full", + "screen", + "cover", + "say", + "render", + "capsule", + "while", + "edge", + "still", + "reads", + "measured", + "duo", + "simulator", + "closed", + "add", + "layer", + "brings", + "two", + "effects", + "paints", + "opaque", + "background", + "over", + "whatever", + "behind", + "apply", + "clear", + "applied", + "itself", + "had", + "effect", + "gives", + "window", + "safe", + "insets", + "back", + "even", + "ancestor", + "ignores", + "84", + "pt", + "34", + "outside", + "read", + "zero", + "re", + "if", + "layout", + "depends", + "swift", + "struct", + "player", + "var", + "body", + "some", + "artwork", + "hidden", + "item", + "button", + "shuffle", + "image", "order", "top", - "bottom", - "back", "automatic", - "container", "close", - "swift", "cancella", "action", - "item", "supple", - "button", "false", "prominent", "pinned", @@ -702049,7 +703270,6 @@ "comple", "title", "scan", - "image", "name", "doc", "viewfinder", @@ -702064,13 +703284,10 @@ "count", "26", "symbol", - "still", "shows", - "itself", "main", "actor", "isolated", - "even", "can", "nonisolated", "model", @@ -702105,7 +703322,6 @@ "segmented", "unless", "opted", - "edge", "compression", "fixed", "width", @@ -702116,21 +703332,16 @@ "must", "fit", "adapt", - "layout", "spacers", "collapse", - "zero", "vertically", "their", "minimum", - "add", "extra", "spac", "supply", "scroll", - "effect", "gain", - "background", "reduce", "transparency", "legible", @@ -702139,9 +703350,37 @@ "hero", "extends", "extension", + "what", + "styl", + "survives", + "ignore", + "toggle", + "foreground", + "style", + "opacity", + "lit", + "disc", + "context", + "does", + "noth", + "opt", + "behavior", + "own", + "all", + "work", + "route", + "unavailable", + "states", + "primary", + "state", + "reported", + "each", + "taken", + "matters", + "overflows", + "repeat", "plan", "landscape", - "overflows", "most", "decide", "per", @@ -702153,7 +703392,6 @@ "task", "into", "merge", - "own", "ellipsis", "rank", "frequent", @@ -702162,6 +703400,15 @@ "should", "last", "up", + "works", + "too", + "ten", + "seven", + "slot", + "marked", + "stayed", + "three", + "overflowed", "non", "always", "open", @@ -702174,64 +703421,75 @@ "calculator", "whose", "may", - "work", "better", "inferred", "usually", "override", "control", - "toggle", "needs", "specific", - "all", - "whatever", "infers", "knobs", "build", "earlier", "sees", - "read", + "trait", + "collec", + "reports", + "regardless", + "currently", + "visible", + "unspecified", + "places", + "without", + "size", + "class", + "orienta", + "use", + "header", + "value", + "tell", + "before", + "any", + "exists", + "half", + "folded", + "included", + "briefly", + "dur", + "transi", + "animate", + "transient", + "mean", + "they", "@available", - "struct", "@environ", "private", - "var", - "body", - "some", "summary", "checkmark", "circle", - "behavior", "toggles", "compass", "loca", "north", - "opt", "infer", "prefers", "overlay", "align", - "while", "font", "cap", "disabled", "screens", "preference", "final", - "class", - "player", "@main", "func", "tune", "traits", - "trait", - "collec", "guard", "available", "return", "compress", - "if", - "unspecified", "inset", "holds" ], @@ -702873,6 +704131,127 @@ "incompatible", "different", "product", + "only", + "creat", + "17", + "pro", + "17e", + "pad", + "fails", + "same", + "sim", + "error", + "403", + "so", + "keep", + "every", + "other", + "measured", + "poses", + "no", + "simctl", + "devicectl", + "command", + "opens", + "closes", + "folds", + "motion", + "hinge", + "angle", + "reads", + "orienta", + "set", + "prints", + "success", + "changes", + "noth", + "closed", + "reported", + "check", + "window", + "size", + "after", + "pose", + "change", + "clos", + "landscape", + "been", + "land", + "portrait", + "one", + "time", + "another", + "screenshots", + "default", + "inner", + "display", + "which", + "black", + "while", + "io", + "screenshot", + "capture", + "alike", + "outer", + "name", + "axiom", + "tools", + "skills", + "control", + "ref", + "md", + "masks", + "multi", + "devices", + "never", + "turned", + "left", + "right", + "upright", + "edge", + "camera", + "status", + "bar", + "sit", + "top", + "vertical", + "runs", + "down", + "trail", + "toolbar", + "along", + "bottom", + "moves", + "lead", + "puts", + "follows", + "synthetic", + "taps", + "send", + "physical", + "touch", + "xcui", + "tap", + "does", + "bare", + "axe", + "needs", + "style", + "items", + "ignore", + "reports", + "styles", + "overflow", + "button", + "did", + "not", + "under", + "any", + "hold", + "overflowed", + "actions", + "room", + "them", "gaps", "per", "beta", @@ -702896,10 +704275,7 @@ "ui", "15", "see", - "axiom", - "uikit", - "skills", - "md" + "uikit" ], "Pressure Scenarios": [ "pressure", @@ -727680,12 +729056,26 @@ "mp4", "below", "orienta", + "set", "get", "also", - "set", "rotate", - "without", - "enter", + "except", + "duo", + "rotat", + "headlessly", + "verb", + "motion", + "hinge", + "angle", + "streams", + "closed", + "180", + "open", + "60", + "default", + "noth", + "sets", "settings", "biometrics", "enable", @@ -727769,7 +729159,76 @@ "reported", "simulators", "treat", - "skip" + "skip", + "keystroke", + "automa", + "substitute", + "while", + "host", + "verify", + "dimensions", + "landscape", + "left", + "portrait", + "upside", + "down", + "right", + "io", + "rotated", + "width", + "height", + "must", + "swap", + "2026", + "09", + "19", + "17", + "1206", + "2622", + "pro", + "11", + "inch", + "1668", + "2420", + "received", + "new", + "both", + "times", + "ignores", + "prints", + "reads", + "rotates", + "measured", + "inner", + "display", + "too", + "poses", + "through", + "hub", + "pose", + "controls", + "swiftui", + "iphone", + "can", + "own", + "request", + "geometry", + "update", + "refused", + "under", + "window", + "model", + "ui", + "scene", + "domain", + "101", + "uikit", + "moderniza", + "needs", + "should", + "har", + "above", + "inside" ], "Screen capture — screenshot & video": [ "screen", @@ -727863,11 +729322,18 @@ "unique", "id", "yes", - "pick", "info", "displays", - "omit", + "omitted", + "always", "primary", + "phone", + "duo", + "inner", + "even", + "while", + "closed", + "dark", "codec", "h264", "default", @@ -727884,6 +729350,7 @@ "stop", "after", "seconds", + "omit", "until", "sigint", "verified", @@ -727922,13 +729389,21 @@ "simctl", "io", "type", + "port", "file", "sim", "writes", "stdout", + "takes", + "uuid", + "enumerate", "video", "mov", "defaults", + "accepted", + "but", + "unsupported", + "renders", "axe", "output", "stream", @@ -727944,7 +729419,79 @@ "mcp", "skills", "ref", - "md" + "md", + "masks", + "multi", + "devices", + "chooses", + "what", + "corners", + "hold", + "transparent", + "user", + "sees", + "glass", + "full", + "framebuffer", + "rectangle", + "captures", + "anyth", + "view", + "leaves", + "under", + "corner", + "lands", + "no", + "channel", + "flat", + "image", + "tools", + "drop", + "compare", + "judg", + "content", + "near", + "also", + "measure", + "outer", + "fully", + "run", + "along", + "top", + "bottom", + "rows", + "18", + "px", + "left", + "185", + "right", + "two", + "needs", + "named", + "alike", + "2007", + "2853", + "which", + "1398", + "2034", + "explicitly", + "bash", + "one", + "wide", + "lcd", + "shows", + "active", + "different", + "identifiers", + "wants", + "guess", + "names", + "fails", + "rejects", + "external", + "hang", + "without", + "frame" ], "Status bar for screenshots": [ "status", @@ -733082,16 +734629,17 @@ "type", "swipe", "forward", - "verbatim", - "so", - "get", - "real", - "hid", + "physical", "touch", + "supplied", + "see", + "styles", + "so", + "lands", "without", - "hav", - "manage", + "manag", "environ", + "flags", "yourself" ], "Invocation": [ @@ -733167,6 +734715,11 @@ "flags", "passthrough", "forwards", + "add", + "style", + "physical", + "every", + "styles", "brew", "cameroncooke", "then", @@ -733213,30 +734766,66 @@ "silent", "exit", "ready", + "drive", + "someth", + "would", + "stop", + "device", + "command", "no", - "see", + "several", + "udid", + "too", + "old", + "tap", + "style", + "sends", + "added", + "read", "problems", "next", "steps", "json", - "when", - "several", - "sims", + "lists", "every", + "so", + "can", + "picked", + "without", + "pars", + "prose", + "more", + "than", + "one", + "pass", "verb", - "targets", - "lowest", - "udid", - "deterministically", - "adds", - "note", - "list", - "them", - "id", - "accepted", - "included", - "specific", - "one" + "refuses", + "each", + "name", + "runtime", + "retry", + "step", + "rather", + "guess", + "because", + "drives", + "wrong", + "while", + "still", + "prints", + "optional", + "doesn", + "refuse", + "reports", + "devices", + "fails", + "gate", + "leav", + "empty", + "noth", + "may", + "targeted", + "state" ], "Subcommands": [ "subcommands", @@ -733350,6 +734939,8 @@ "tap", "right", "button", + "physical", + "touch", "prefers", "most", "permissive", @@ -733770,9 +735361,12 @@ "verbs", "forward", "xe", - "verbatim", - "same", "flags", + "pass", + "go", + "through", + "untouched", + "same", "output", "exit", "code", @@ -733786,7 +735380,9 @@ "developer", "dir", "handl", - "comes", + "physical", + "style", + "come", "along", "automatically", "so", @@ -733810,18 +735406,28 @@ "injected", "when", "omitted", + "exactly", + "one", + "sim", + "booted", "help", "shows", "own", "bash", "id", "login", - "real", - "hid", - "pointer", - "hover", + "down", + "up", "user@example", "com", + "200", + "400", + "delay", + "long", + "press", + "no", + "hold", + "option", "describe", "ui", "raw", @@ -733830,23 +735436,124 @@ "assert", "wait", "parse", + "styles", + "takes", + "automatic", + "default", + "sends", + "only", + "switches", + "toggles", + "fb", + "everyth", + "else", + "activated", + "none", + "targets", + "below", + "swift", + "target", + "toggle", + "switch", + "fires", + "effect", + "list", + "row", + "naviga", + "link", + "pushes", + "inside", + "menu", + "opens", + "open", + "tab", + "view", + "phone", + "selects", + "documented", + "behavior", + "other", + "rest", + "measured", + "2026", + "09", + "19", + "xcode", + "27", + "17", + "os", + "duo", + "simulators", + "where", + "printed", + "completed", + "successfully", + "every", + "cell", + "adds", + "override", + "dialog", + "taps", + "physically", + "yourself", + "add", + "verify", + "never", + "changes", + "noth", + "prints", + "success", + "line", + "works", + "after", + "any", + "matters", + "confirm", + "result", + "ele", + "selectors", + "accessibility", + "frame", + "pixels", + "label", + "resolve", + "activa", + "point", + "skewed", + "hidden", + "fixed", + "size", + "plus", + "width", + "clipped", + "case", + "landed", + "about", + "pt", + "visible", + "control", + "misses", "still", + "measure", + "center", + "points", + "fix", + "matches", + "what", + "draws", "stream", "video", "record", "stay", "bare", "first", - "what", "itself", "parses", "two", "outlive", - "any", "request", "timeout", "those", - "only", "calls", "need", "prefix", @@ -733855,8 +735562,6 @@ "rare", "loads", "under", - "xcode", - "27", "unaided" ], "Output & exit codes": [ @@ -744445,6 +746150,40 @@ "changes", "resolved", "state", + "request", + "refuses", + "under", + "landscape", + "right", + "calls", + "error", + "handler", + "domain", + "code", + "101", + "current", + "mode", + "does", + "allow", + "programmatic", + "keeps", + "signal", + "always", + "pass", + "one", + "same", + "rotated", + "measured", + "pro", + "11", + "inch", + "17", + "simulators", + "lay", + "re", + "given", + "instead", + "forc", "interactive", "vs", "settled", @@ -760574,6 +762313,8 @@ "axe", "login", "button", + "style", + "physical", "udid" ], "3. OR: Tap by label": [ @@ -760581,6 +762322,8 @@ "label", "axe", "login", + "style", + "physical", "udid" ], "4. LAST RESORT: Tap by coordinates from describe-ui output": [ @@ -760594,7 +762337,52 @@ "axe", "200", "400", + "style", + "physical", "udid", + "always", + "pass", + "xe", + "default", + "automatic", + "sends", + "fb", + "simulator", + "anyth", + "isn", + "switch", + "xcode", + "27", + "activated", + "no", + "swift", + "button", + "list", + "row", + "menu", + "tab", + "while", + "print", + "completed", + "successfully", + "touch", + "down", + "up", + "all", + "them", + "measured", + "2026", + "09", + "19", + "matrix", + "axiom", + "tools", + "skills", + "xcui", + "ref", + "md", + "adds", + "flag", "priority", "order", "target", @@ -760612,8 +762400,7 @@ "fragile", "use", "only", - "when", - "no" + "when" ], "Core Concept: Accessibility-First": [ "core", @@ -760643,6 +762430,8 @@ "tap", "200", "400", + "style", + "physical", "udid" ], "Accessibility-based (stable - survives UI changes)": [ @@ -760657,12 +762446,44 @@ "id", "login", "button", + "style", + "physical", "udid", "label", "always", "prefer", "over", - "coordinates" + "coordinates", + "one", + "caveat", + "selectors", + "ele", + "activa", + "point", + "not", + "pixels", + "view", + "whose", + "frame", + "skewed", + "hidden", + "fixed", + "size", + "width", + "clipped", + "gets", + "tapped", + "off", + "target", + "still", + "prints", + "when", + "selector", + "noth", + "screenshot", + "measure", + "visible", + "center" ], "Getting the Simulator UDID": [ "gett", @@ -760674,27 +762495,53 @@ "commands", "bash" ], - "Get booted simulator UDID": [ - "get", + "List booted simulators, then pick one by name — never `head -1`": [ + "list", "booted", - "simulator", - "udid", + "simulators", + "then", + "pick", + "one", + "name", + "never", + "head", "xcrun", "simctl", - "list", "devices", - "jq", - "entries", - "value", - "select", - "state", - "head" + "udid", + "device", + "mean" ], "List all simulators": [ "list", "all", "simulators", - "axe" + "axe", + "more", + "than", + "one", + "simulator", + "booted", + "head", + "pick", + "drives", + "whichever", + "device", + "sorts", + "first", + "every", + "tap", + "still", + "prints", + "xcui", + "refuses", + "guess", + "case", + "direct", + "xe", + "calls", + "need", + "choose" ], "Touch & Tap Commands": [ "touch", @@ -760714,6 +762561,8 @@ "id", "login", "button", + "style", + "physical", "udid" ], "Tap element with accessibility label": [ @@ -760723,6 +762572,8 @@ "label", "axe", "submit", + "style", + "physical", "udid", "coordinates", "bash" @@ -760733,6 +762584,8 @@ "axe", "200", "400", + "style", + "physical", "udid" ], "Tap with timing controls": [ @@ -760742,12 +762595,14 @@ "axe", "200", "400", + "style", + "physical", "pre", "delay", "post", "udid" ], - "Long press: use touch with --down --up --delay (tap has no hold option)": [ + "Long press: use touch with --down --up --delay (tap has no hold option);": [ "long", "press", "use", @@ -760758,10 +762613,22 @@ "tap", "no", "hold", - "option", + "option" + ], + "a 1.2 s hold opened a SwiftUI .contextMenu": [ + "hold", + "opened", + "swift", + "ui", + "context", + "menu", "axe", + "touch", "200", "400", + "down", + "up", + "delay", "udid", "low", "level", @@ -761302,6 +763169,8 @@ "axe", "id", "text", + "style", + "physical", "udid", "user@example", "com" @@ -761314,6 +763183,8 @@ "axe", "id", "text", + "style", + "physical", "udid", "password123" ], @@ -761323,6 +763194,8 @@ "button", "axe", "id", + "style", + "physical", "udid" ], "Wait and screenshot": [ @@ -761378,6 +763251,8 @@ "tap", "id", "identifier", + "style", + "physical", "udid", "scroll", "find", @@ -761404,6 +763279,8 @@ "then", "tap", "id", + "style", + "physical", "break", "fi", "gesture", @@ -761423,6 +763300,8 @@ "id", "submit", "button", + "style", + "physical", "udid", "then", "screenshot", @@ -761483,6 +763362,8 @@ "pre", "delay", "post", + "style", + "physical", "udid" ], "AXe vs simctl": [ @@ -761544,19 +763425,55 @@ "tap", "doesn", "work", + "changes", + "noth", + "still", + "prints", + "completed", + "successfully", + "so", + "order", + "style", + "add", + "physical", + "default", + "sends", + "which", + "swift", + "controls", + "ignored", + "every", + "case", + "measured", + "wrong", + "device", + "more", + "than", + "one", + "simulator", + "booted", + "confirm", + "udid", + "names", + "xcrun", + "simctl", + "list", + "devices", + "skewed", + "frame", + "selector", + "resolved", + "but", + "point", + "control", + "instead", "enabled", "true", "try", - "add", "pre", "delay", "slow", "load", - "verify", - "correct", - "udid", - "list", - "simulators", "type", "text", "field", @@ -765681,7 +767598,7 @@ }, { "name": "axiom-tools--xcui-ref", - "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe verbatim, so you get AXe's real HID touch without having to manage its environment yourself.", + "description": "xcui makes iOS-simulator UI and accessibility testing scriptable for coding harnesses. It owns the test-harness semantics AXe and simctl lack — waiting, asserting, accessibility config, dialogs, computed VoiceOver — and is also the front door for input: `xcui tap`/`type`/`swipe` forward to AXe, with a physical touch supplied for `tap` (see Tap styles), so input lands without you managing AXe's environment or flags yourself.", "skillType": "reference", "source": "axiom" },