@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/facts.json +2 -2
  3. package/docs/features.md +1 -1
  4. package/docs/recovery-guide.md +1 -1
  5. package/install/catalog-history.json +1 -1
  6. package/manifest.json +225 -148
  7. package/package.json +1 -1
  8. package/pipeline/commands/multi-agent/SKILL.md +33 -154
  9. package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
  10. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
  11. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
  12. package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
  13. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
  14. package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
  15. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
  16. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
  17. package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
  18. package/pipeline/commands/multi-agent/manual-test/SKILL.md +12 -7
  19. package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
  20. package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
  21. package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
  22. package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
  23. package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
  24. package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
  25. package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
  26. package/pipeline/commands/multi-agent/sync/SKILL.md +35 -164
  27. package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
  28. package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
  29. package/pipeline/contract/CHANGELOG.md +9 -0
  30. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  31. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  32. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  33. package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
  34. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  35. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  36. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  37. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  38. package/pipeline/contract/fixtures/runs-running.json +1 -1
  39. package/pipeline/contract/manifest.json +5 -5
  40. package/pipeline/contract/types/index.d.ts +3 -2
  41. package/pipeline/multi-agent-refs/audit-guide.md +20 -0
  42. package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
  43. package/pipeline/multi-agent-refs/channels/generator.md +14 -0
  44. package/pipeline/multi-agent-refs/channels/preview.md +13 -0
  45. package/pipeline/multi-agent-refs/channels/reference.md +46 -0
  46. package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
  47. package/pipeline/multi-agent-refs/design-check/export.md +1 -0
  48. package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
  49. package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
  50. package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
  51. package/pipeline/multi-agent-refs/features/commit-multi-repo.md +63 -0
  52. package/pipeline/multi-agent-refs/features/dev-multi-repo.md +51 -0
  53. package/pipeline/multi-agent-refs/features/test-gap.md +40 -0
  54. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  55. package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
  56. package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
  57. package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
  58. package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
  59. package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
  60. package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
  61. package/pipeline/multi-agent-refs/orchestrator/phase-3-user-test.md +12 -9
  62. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
  63. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +8 -47
  64. package/pipeline/multi-agent-refs/phases/phase-3-review.md +21 -65
  65. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +9 -59
  66. package/pipeline/multi-agent-refs/phases/phase-5-report.md +0 -31
  67. package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
  68. package/pipeline/multi-agent-refs/resume/details.md +115 -2
  69. package/pipeline/multi-agent-refs/review/inputs.md +33 -1
  70. package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
  71. package/pipeline/multi-agent-refs/review/panel.md +75 -0
  72. package/pipeline/multi-agent-refs/review/valven.md +5 -1
  73. package/pipeline/multi-agent-refs/rules.md +1 -1
  74. package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
  75. package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
  76. package/pipeline/multi-agent-refs/stack/background.md +72 -0
  77. package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
  78. package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
  79. package/pipeline/multi-agent-refs/sync/office.md +10 -0
  80. package/pipeline/multi-agent-refs/sync/overview.md +61 -0
  81. package/pipeline/multi-agent-refs/sync/repo.md +91 -0
  82. package/pipeline/multi-agent-refs/update/details.md +83 -0
  83. package/pipeline/office/index.html +6 -3
  84. package/pipeline/office/toolkit.json +1 -1
  85. package/pipeline/rules/figma-pipeline.md +6 -3
  86. package/pipeline/schemas/prefs.schema.json +1 -1
  87. package/pipeline/schemas/runs-index.schema.json +2 -1
  88. package/pipeline/scripts/_memory-policy.mjs +27 -5
  89. package/pipeline/scripts/checkpoint-fold.sh +59 -0
  90. package/pipeline/scripts/contract-server.mjs +1 -1
  91. package/pipeline/scripts/gate-ledger.mjs +1 -1
  92. package/pipeline/scripts/office-control.mjs +41 -15
  93. package/pipeline/scripts/offload-ref.sh +1 -1
  94. package/pipeline/scripts/rework-rulings.mjs +5 -3
  95. package/pipeline/scripts/runs-index.mjs +34 -7
  96. package/pipeline/scripts/scope-check-gate.mjs +15 -2
  97. package/pipeline/scripts/status-overview.mjs +401 -0
  98. package/pipeline/scripts/user-test-handoff.sh +203 -0
  99. package/pipeline/scripts/validate-planning.mjs +5 -3
  100. package/pipeline/scripts/validate-triage.mjs +2 -1
  101. package/pipeline/scripts/write-state.mjs +13 -4
  102. package/pipeline/skills/.skill-manifest.json +64 -64
  103. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +7 -70
  104. package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
  105. package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
  106. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +10 -72
  107. package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
  108. package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
  109. package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
  110. package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
  111. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
  112. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
  113. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
  114. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
  115. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
  116. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +12 -7
  117. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
  118. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
  119. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
  120. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
  121. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +73 -217
  122. package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
  123. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
  124. package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
  125. package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
  126. package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
  127. package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
  128. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
  129. package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
  130. package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
  131. package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
  132. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
  133. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
  134. package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
  135. package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
  136. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
  137. package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
  138. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
  139. package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
  140. package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
  141. package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
  142. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
  143. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
  144. package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
  145. package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
  146. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
  147. package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
  148. package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
  149. package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
  150. package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
  151. package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
  152. package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
  153. package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
  154. package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
  155. package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
  156. package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
  157. package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
  158. package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
  159. package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
  160. package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
  161. package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
  162. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
  163. package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
  164. package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
  165. package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
  166. package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
  167. package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
  168. package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
  169. package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
  170. package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
  171. package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
  172. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
  173. package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
  174. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
  175. package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
  176. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
  177. package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
  178. package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
  179. package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
  180. package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
  181. package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
  182. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
  183. package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
  184. package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
  185. package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
  186. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
  187. package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
  188. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
  189. package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
  191. package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
  193. package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
  194. package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
  195. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
  196. package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
  197. package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
  198. package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
  199. package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
  200. package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
  201. package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
  202. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
  203. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
  204. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
  205. package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
  206. package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
  207. package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
  208. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
  209. package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
  210. package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
  211. package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
  212. package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
  213. package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
  214. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
  215. package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
  216. package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
  217. package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
  218. package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
  219. package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
  220. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
  221. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
  222. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
@@ -15,6 +15,7 @@ UIKit and AppKit.
15
15
 
16
16
  References:
17
17
 
18
+ - [Assistive technology review](references/assistive-tech-review.md): Reduce Motion and appearance code, the per-technology review rules (Voice Control, Switch Control, Full Keyboard Access, Assistive Access), UIKit and AppKit specifics, custom content, and the manual and XCTest test steps.
18
19
  - [Code patterns](references/a11y-patterns.md): worked examples of the
19
20
  modifiers in both UI frameworks, grouping, custom and adjustable controls, custom actions, rotors,
20
21
  Dynamic Type, AppKit, Voice Control, Switch Control, Full Keyboard Access,
@@ -184,46 +185,16 @@ unread threads or headings. Add them to screens dense with content; see
184
185
 
185
186
  With Reduce Motion on, replace movement with a crossfade or with no animation.
186
187
 
187
- ```swift
188
- struct FilterDrawer: View {
189
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
190
- @State private var isExpanded = false
191
-
192
- var body: some View {
193
- VStack {
194
- Button("Filters") {
195
- withAnimation(reduceMotion ? nil : .spring()) { isExpanded.toggle() }
196
- }
197
- if isExpanded {
198
- PanelView()
199
- .transition(reduceMotion ? .opacity : .move(edge: .bottom))
200
- }
201
- }
202
- }
203
- }
204
- ```
205
-
206
188
  Go through every moving transition, not just the obvious one: row removal,
207
189
  quantity changes, presenting a sheet or checkout, dismissing a modal. Slides,
208
190
  bounces, parallax, springs and big spatial moves become fades, an immediate
209
191
  jump to the end state, or no animation at all.
210
192
 
211
- ### Transparency, contrast, bold text
193
+ A `FilterDrawer` example that swaps the spring and slide for no animation and a fade: [Assistive technology review](references/assistive-tech-review.md).
212
194
 
213
- ```swift
214
- @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
215
- @Environment(\.colorSchemeContrast) private var contrast
216
- @Environment(\.legibilityWeight) private var legibilityWeight
217
-
218
- private let base = Color(uiColor: .systemBackground)
195
+ ### Transparency, contrast, bold text
219
196
 
220
- var body: some View {
221
- Text(caption)
222
- .foregroundStyle(contrast == .increased ? .primary : .secondary)
223
- .fontWeight(legibilityWeight == .bold ? .bold : .regular)
224
- .background(reduceTransparency ? base : base.opacity(0.8))
225
- }
226
- ```
197
+ Code that adapts foreground style, font weight and background opacity to these three settings: [Assistive technology review](references/assistive-tech-review.md).
227
198
 
228
199
  ## Decorative versus meaningful images
229
200
 
@@ -244,62 +215,15 @@ instead of hiding it.
244
215
 
245
216
  ## Voice Control
246
217
 
247
- Spoken commands find their target through its accessibility label: the label
248
- is the name a person says to tap the element. An element with no label, or a label nobody can say, cannot be
249
- targeted by voice.
250
-
251
- - Every interactive element needs a label a person can say aloud. No labels
252
- made only of emoji or symbols.
253
- - Labels must differ among the elements on screen at the same time;
254
- duplicates push users onto overlay numbers.
255
- - Add `accessibilityInputLabels` before feature freeze, not as late polish,
256
- wherever a label is long, clumsy, localized, full of acronyms, or usually
257
- shortened when spoken. Both Full Keyboard Access and Voice Control consume
258
- them. List the entries most important first.
259
- - Good candidates: media controls, settings and account links, per-row
260
- actions that repeat down a list, quantity steppers, localized labels that
261
- contain product names or acronyms.
262
- - Turn Voice Control on and say "Show Names", then "Show Numbers". The names
263
- overlay proves labels can be spoken; the numbers overlay proves every
264
- visible target stays reachable even if its name is missing, duplicated or
265
- awkward. A review
266
- checks both overlays.
267
-
268
- Examples and speakable-label guidance: [code patterns](references/a11y-patterns.md).
218
+ Every interactive element needs a unique label a person can say aloud; add `accessibilityInputLabels` early for long, localized or acronym-heavy labels, and check both the Show Names and Show Numbers overlays. Full rules: [Assistive technology review](references/assistive-tech-review.md).
269
219
 
270
220
  ## Switch Control
271
221
 
272
- Switch Control steps through accessibility elements one at a time in reading
273
- order, so grouping and custom actions matter more here than anywhere else.
274
-
275
- - `.accessibilityElement(children: .combine)` merges related pieces and
276
- removes scan stops. Each remaining stop should mean something and do
277
- something.
278
- - Anything hidden from VoiceOver as decorative is skipped by Switch Control
279
- too.
280
- - Multi-finger gestures, long presses and swipe-to-delete are out of reach
281
- for switch users. Offer those operations as `.accessibilityAction(named:)`; Switch
282
- Control lists them in a menu.
283
- - In point scanning mode, a custom control whose tappable area is unusual
284
- must report an `accessibilityFrame` that matches that area.
222
+ Scanning follows reading order: combine related pieces to cut scan stops, and offer gestures (multi-finger, long press, swipe-to-delete) as named custom actions. Full rules: [Assistive technology review](references/assistive-tech-review.md).
285
223
 
286
224
  ## Full Keyboard Access
287
225
 
288
- Available since iOS and iPadOS 13.4, Full Keyboard Access lets people run the
289
- app from a hardware keyboard. This skill owns the review: can everything be
290
- reached, is it clearly labelled, is focus visible, does it work without touch.
291
- `UIFocusGuide`, tvOS focus, scene-focused values, directional movement,
292
- `focusSection()`, `@FocusState`, `.focusable()`, custom cards that Tab jumps
293
- over, and Tab traversal bugs in general go to `hig-inputs` first; only the accessibility finding
294
- stays here.
295
-
296
- Audit:
297
-
298
- - the keyboard alone can reach and trigger every interactive element;
299
- - the traversal order makes sense and never traps focus;
300
- - focus indicators stay visible at every contrast and text-size setting;
301
- - anything that only works through a gesture has a keyboard route;
302
- - the app's own shortcuts leave system ones (Cmd+C, Cmd+V, Cmd+Tab) alone.
226
+ Audit that the keyboard alone reaches and triggers everything, order never traps focus, focus stays visible, gestures have a keyboard route, and system shortcuts stay free; focus-system mechanics go to `hig-inputs`. Full audit list: [Assistive technology review](references/assistive-tech-review.md).
303
227
 
304
228
  ## Traversal order
305
229
 
@@ -314,70 +238,19 @@ the ordering audit and its accessibility impact are reported here.
314
238
 
315
239
  ## Assistive Access (iOS 18+)
316
240
 
317
- This pared-down system mode serves people with cognitive disabilities, and
318
- apps should behave well inside it.
319
-
320
- ```swift
321
- struct LibraryScreen: View {
322
- @Environment(\.accessibilityAssistiveAccessEnabled) private var assistiveAccess
323
-
324
- var body: some View {
325
- if assistiveAccess { SimpleLibraryView() } else { FullLibraryView() }
326
- }
327
- }
328
- ```
329
-
330
- - Less on screen: fewer controls, bigger targets, flatter navigation.
331
- - Literal, plain wording in labels and instructions.
332
- - Only a few choices at a time.
333
- - Try it under Settings > Accessibility > Assistive Access.
334
-
335
- From iOS 26 an app can also declare `UISupportsAssistiveAccess` in its
336
- Info.plist and provide a dedicated `AssistiveAccess` scene.
241
+ Read `accessibilityAssistiveAccessEnabled` and offer a simpler screen: fewer controls, bigger targets, literal wording, few choices. Example and the iOS 26 scene option: [Assistive technology review](references/assistive-tech-review.md).
337
242
 
338
243
  ## UIKit
339
244
 
340
- - `isAccessibilityElement = true` on custom views that carry meaning.
341
- - `accessibilityLabel` on every interactive element without visible text.
342
- - Change traits with `.insert()` and `.remove()` instead of assigning:
343
- `badgeView.accessibilityTraits.insert(.button)`,
344
- `badgeView.accessibilityTraits.remove(.staticText)`.
345
- - `accessibilityViewIsModal = true` on custom overlays to keep focus inside.
346
- - Post `.announcement` for passing status, `.layoutChanged` with a target for
347
- partial updates, `.screenChanged` for full transitions.
245
+ Custom views that carry meaning set `isAccessibilityElement`, labels and traits (insert and remove, never assign), overlays set `accessibilityViewIsModal`, and changes post the matching notification. Details: [Assistive technology review](references/assistive-tech-review.md).
348
246
 
349
247
  ## AppKit
350
248
 
351
- AppKit accessibility rests on `NSAccessibilityProtocol` plus role-specific
352
- protocols. Standard controls already publish most of what assistive apps need,
353
- including notifications, so prefer them and customise only where the defaults
354
- fall short.
355
-
356
- - A custom `NSView` adopts the right role behaviour and reports its role,
357
- label, value and supported actions accurately.
358
- - Items with no view behind them use `NSAccessibilityElement`.
359
- - Post `NSAccessibility` notifications when a change must reach assistive
360
- apps.
361
-
362
- The full custom-view example (a toggle-like `NSView` returning role, label,
363
- "On"/"Off" value and a press action, posting `.valueChanged` when its state
364
- changes) and `NSAccessibilityElement` usage are in
365
- [code patterns](references/a11y-patterns.md#appkit).
249
+ Prefer standard controls; custom `NSView`s report role, label, value and actions, viewless items use `NSAccessibilityElement`, and changes post `NSAccessibility` notifications. Details: [Assistive technology review](references/assistive-tech-review.md).
366
250
 
367
251
  ## Custom content
368
252
 
369
- `.accessibilityCustomContent` attaches extra labelled facts to an element.
370
- By default VoiceOver offers them only on request (More Content rotor); with
371
- `importance: .high` they are read along with the element.
372
-
373
- ```swift
374
- HotelCard(hotel: hotel)
375
- .accessibilityCustomContent("Nightly rate", Text(hotel.rateText), importance: .high)
376
- .accessibilityCustomContent("Guest score", Text(hotel.scoreText))
377
- ```
378
-
379
- More UIKit, AppKit and custom-content examples:
380
- [code patterns](references/a11y-patterns.md).
253
+ `.accessibilityCustomContent` adds labelled facts, offered on request unless `importance: .high`. Example: [Assistive technology review](references/assistive-tech-review.md).
381
254
 
382
255
  ## App Store Accessibility Nutrition Labels
383
256
 
@@ -396,41 +269,11 @@ only recommended after you:
396
269
 
397
270
  ### By hand
398
271
 
399
- | Tool | What to do | Where |
400
- |---|---|---|
401
- | Accessibility Inspector | Run an audit against the Simulator or a device; it reports contrast issues, absent labels and wrong traits | Xcode > Open Developer Tool |
402
- | VoiceOver | Swipe from the first element to the last on each screen | Settings > Accessibility > VoiceOver |
403
- | Voice Control | Say "Show Names", then "Show Numbers" | Settings > Accessibility > Voice Control |
404
- | Full Keyboard Access | Press Tab across each screen and confirm each control receives focus | Settings > Accessibility > Keyboards > Full Keyboard Access |
405
- | Switch Control | Watch the scan order; gesture features need custom actions | Settings > Accessibility > Switch Control |
406
- | Dynamic Type | Step through the whole size range | Settings > Accessibility > Display & Text Size > Larger Text |
272
+ Accessibility Inspector, VoiceOver, Voice Control, Full Keyboard Access, Switch Control and the Dynamic Type range, each checked per screen. Tool table: [Assistive technology review](references/assistive-tech-review.md).
407
273
 
408
274
  ### With XCTest
409
275
 
410
- UI tests can assert accessibility through `XCUIElement` attributes. The useful
411
- ones in `XCUIElementAttributes` are `identifier`, `label`, `title`, `value`,
412
- `placeholderValue`, `isSelected`, `isEnabled` and `hasFocus`.
413
-
414
- ```swift
415
- @MainActor
416
- func testFocusReturnsAfterSheet() {
417
- let app = XCUIApplication()
418
- app.launch()
419
-
420
- let opener = app.buttons["Seat map"]
421
- opener.tap()
422
- app.buttons["Done"].tap()
423
- XCTAssertTrue(opener.hasFocus)
424
- }
425
- ```
426
-
427
- A row test (querying `app.cells["id"]`, asserting `exists`, `isEnabled`, a
428
- non-empty `label` and a nested `buttons[...]` query) is in
429
- [code patterns](references/a11y-patterns.md#automated-checks-with-xctest).
430
-
431
- `hasFocus` reports keyboard focus (Full Keyboard Access, hardware keyboard),
432
- not the VoiceOver cursor, which XCTest cannot observe. Confirm VoiceOver focus
433
- restoration by hand as well.
276
+ `XCUIElement` attributes assert identifiers, labels, values and keyboard focus; `hasFocus` does not see the VoiceOver cursor, so confirm that by hand. Example: [Assistive technology review](references/assistive-tech-review.md).
434
277
 
435
278
  ## Mistakes seen most often
436
279
 
@@ -0,0 +1,224 @@
1
+ # Assistive technology review
2
+
3
+ <!-- toc -->
4
+ - [Reduce Motion](#reduce-motion)
5
+ - [Transparency, contrast, bold text](#transparency-contrast-bold-text)
6
+ - [Voice Control](#voice-control)
7
+ - [Switch Control](#switch-control)
8
+ - [Full Keyboard Access](#full-keyboard-access)
9
+ - [Assistive Access (iOS 18+)](#assistive-access-ios-18)
10
+ - [UIKit](#uikit)
11
+ - [AppKit](#appkit)
12
+ - [Custom content](#custom-content)
13
+ <!-- /toc -->
14
+
15
+ Reduce Motion and appearance code, the review rules for Voice Control, Switch Control, Full Keyboard Access and Assistive Access, the UIKit and AppKit specifics, custom content, and the manual and XCTest testing steps. SKILL.md keeps the ground rules, focus, traversal order and the review checklist.
16
+
17
+ ## Reduce Motion
18
+
19
+ With Reduce Motion on, replace movement with a crossfade or with no animation.
20
+
21
+ ```swift
22
+ struct FilterDrawer: View {
23
+ @Environment(\.accessibilityReduceMotion) private var reduceMotion
24
+ @State private var isExpanded = false
25
+
26
+ var body: some View {
27
+ VStack {
28
+ Button("Filters") {
29
+ withAnimation(reduceMotion ? nil : .spring()) { isExpanded.toggle() }
30
+ }
31
+ if isExpanded {
32
+ PanelView()
33
+ .transition(reduceMotion ? .opacity : .move(edge: .bottom))
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Go through every moving transition, not just the obvious one: row removal,
41
+ quantity changes, presenting a sheet or checkout, dismissing a modal. Slides,
42
+ bounces, parallax, springs and big spatial moves become fades, an immediate
43
+ jump to the end state, or no animation at all.
44
+
45
+ ## Transparency, contrast, bold text
46
+
47
+ ```swift
48
+ @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
49
+ @Environment(\.colorSchemeContrast) private var contrast
50
+ @Environment(\.legibilityWeight) private var legibilityWeight
51
+
52
+ private let base = Color(uiColor: .systemBackground)
53
+
54
+ var body: some View {
55
+ Text(caption)
56
+ .foregroundStyle(contrast == .increased ? .primary : .secondary)
57
+ .fontWeight(legibilityWeight == .bold ? .bold : .regular)
58
+ .background(reduceTransparency ? base : base.opacity(0.8))
59
+ }
60
+ ```
61
+
62
+ ## Voice Control
63
+
64
+ Spoken commands find their target through its accessibility label: the label
65
+ is the name a person says to tap the element. An element with no label, or a label nobody can say, cannot be
66
+ targeted by voice.
67
+
68
+ - Every interactive element needs a label a person can say aloud. No labels
69
+ made only of emoji or symbols.
70
+ - Labels must differ among the elements on screen at the same time;
71
+ duplicates push users onto overlay numbers.
72
+ - Add `accessibilityInputLabels` before feature freeze, not as late polish,
73
+ wherever a label is long, clumsy, localized, full of acronyms, or usually
74
+ shortened when spoken. Both Full Keyboard Access and Voice Control consume
75
+ them. List the entries most important first.
76
+ - Good candidates: media controls, settings and account links, per-row
77
+ actions that repeat down a list, quantity steppers, localized labels that
78
+ contain product names or acronyms.
79
+ - Turn Voice Control on and say "Show Names", then "Show Numbers". The names
80
+ overlay proves labels can be spoken; the numbers overlay proves every
81
+ visible target stays reachable even if its name is missing, duplicated or
82
+ awkward. A review
83
+ checks both overlays.
84
+
85
+ Examples and speakable-label guidance: [code patterns](a11y-patterns.md).
86
+
87
+ ## Switch Control
88
+
89
+ Switch Control steps through accessibility elements one at a time in reading
90
+ order, so grouping and custom actions matter more here than anywhere else.
91
+
92
+ - `.accessibilityElement(children: .combine)` merges related pieces and
93
+ removes scan stops. Each remaining stop should mean something and do
94
+ something.
95
+ - Anything hidden from VoiceOver as decorative is skipped by Switch Control
96
+ too.
97
+ - Multi-finger gestures, long presses and swipe-to-delete are out of reach
98
+ for switch users. Offer those operations as `.accessibilityAction(named:)`; Switch
99
+ Control lists them in a menu.
100
+ - In point scanning mode, a custom control whose tappable area is unusual
101
+ must report an `accessibilityFrame` that matches that area.
102
+
103
+ ## Full Keyboard Access
104
+
105
+ Available since iOS and iPadOS 13.4, Full Keyboard Access lets people run the
106
+ app from a hardware keyboard. This skill owns the review: can everything be
107
+ reached, is it clearly labelled, is focus visible, does it work without touch.
108
+ `UIFocusGuide`, tvOS focus, scene-focused values, directional movement,
109
+ `focusSection()`, `@FocusState`, `.focusable()`, custom cards that Tab jumps
110
+ over, and Tab traversal bugs in general go to `hig-inputs` first; only the accessibility finding
111
+ stays here.
112
+
113
+ Audit:
114
+
115
+ - the keyboard alone can reach and trigger every interactive element;
116
+ - the traversal order makes sense and never traps focus;
117
+ - focus indicators stay visible at every contrast and text-size setting;
118
+ - anything that only works through a gesture has a keyboard route;
119
+ - the app's own shortcuts leave system ones (Cmd+C, Cmd+V, Cmd+Tab) alone.
120
+
121
+ ## Assistive Access (iOS 18+)
122
+
123
+ This pared-down system mode serves people with cognitive disabilities, and
124
+ apps should behave well inside it.
125
+
126
+ ```swift
127
+ struct LibraryScreen: View {
128
+ @Environment(\.accessibilityAssistiveAccessEnabled) private var assistiveAccess
129
+
130
+ var body: some View {
131
+ if assistiveAccess { SimpleLibraryView() } else { FullLibraryView() }
132
+ }
133
+ }
134
+ ```
135
+
136
+ - Less on screen: fewer controls, bigger targets, flatter navigation.
137
+ - Literal, plain wording in labels and instructions.
138
+ - Only a few choices at a time.
139
+ - Try it under Settings > Accessibility > Assistive Access.
140
+
141
+ From iOS 26 an app can also declare `UISupportsAssistiveAccess` in its
142
+ Info.plist and provide a dedicated `AssistiveAccess` scene.
143
+
144
+ ## UIKit
145
+
146
+ - `isAccessibilityElement = true` on custom views that carry meaning.
147
+ - `accessibilityLabel` on every interactive element without visible text.
148
+ - Change traits with `.insert()` and `.remove()` instead of assigning:
149
+ `badgeView.accessibilityTraits.insert(.button)`,
150
+ `badgeView.accessibilityTraits.remove(.staticText)`.
151
+ - `accessibilityViewIsModal = true` on custom overlays to keep focus inside.
152
+ - Post `.announcement` for passing status, `.layoutChanged` with a target for
153
+ partial updates, `.screenChanged` for full transitions.
154
+
155
+ ## AppKit
156
+
157
+ AppKit accessibility rests on `NSAccessibilityProtocol` plus role-specific
158
+ protocols. Standard controls already publish most of what assistive apps need,
159
+ including notifications, so prefer them and customise only where the defaults
160
+ fall short.
161
+
162
+ - A custom `NSView` adopts the right role behaviour and reports its role,
163
+ label, value and supported actions accurately.
164
+ - Items with no view behind them use `NSAccessibilityElement`.
165
+ - Post `NSAccessibility` notifications when a change must reach assistive
166
+ apps.
167
+
168
+ The full custom-view example (a toggle-like `NSView` returning role, label,
169
+ "On"/"Off" value and a press action, posting `.valueChanged` when its state
170
+ changes) and `NSAccessibilityElement` usage are in
171
+ [code patterns](a11y-patterns.md#appkit).
172
+
173
+ ## Custom content
174
+
175
+ `.accessibilityCustomContent` attaches extra labelled facts to an element.
176
+ By default VoiceOver offers them only on request (More Content rotor); with
177
+ `importance: .high` they are read along with the element.
178
+
179
+ ```swift
180
+ HotelCard(hotel: hotel)
181
+ .accessibilityCustomContent("Nightly rate", Text(hotel.rateText), importance: .high)
182
+ .accessibilityCustomContent("Guest score", Text(hotel.scoreText))
183
+ ```
184
+
185
+ More UIKit, AppKit and custom-content examples:
186
+ [code patterns](a11y-patterns.md).
187
+
188
+ ### By hand
189
+
190
+ | Tool | What to do | Where |
191
+ |---|---|---|
192
+ | Accessibility Inspector | Run an audit against the Simulator or a device; it reports contrast issues, absent labels and wrong traits | Xcode > Open Developer Tool |
193
+ | VoiceOver | Swipe from the first element to the last on each screen | Settings > Accessibility > VoiceOver |
194
+ | Voice Control | Say "Show Names", then "Show Numbers" | Settings > Accessibility > Voice Control |
195
+ | Full Keyboard Access | Press Tab across each screen and confirm each control receives focus | Settings > Accessibility > Keyboards > Full Keyboard Access |
196
+ | Switch Control | Watch the scan order; gesture features need custom actions | Settings > Accessibility > Switch Control |
197
+ | Dynamic Type | Step through the whole size range | Settings > Accessibility > Display & Text Size > Larger Text |
198
+
199
+ ### With XCTest
200
+
201
+ UI tests can assert accessibility through `XCUIElement` attributes. The useful
202
+ ones in `XCUIElementAttributes` are `identifier`, `label`, `title`, `value`,
203
+ `placeholderValue`, `isSelected`, `isEnabled` and `hasFocus`.
204
+
205
+ ```swift
206
+ @MainActor
207
+ func testFocusReturnsAfterSheet() {
208
+ let app = XCUIApplication()
209
+ app.launch()
210
+
211
+ let opener = app.buttons["Seat map"]
212
+ opener.tap()
213
+ app.buttons["Done"].tap()
214
+ XCTAssertTrue(opener.hasFocus)
215
+ }
216
+ ```
217
+
218
+ A row test (querying `app.cells["id"]`, asserting `exists`, `isEnabled`, a
219
+ non-empty `label` and a nested `buttons[...]` query) is in
220
+ [code patterns](a11y-patterns.md#automated-checks-with-xctest).
221
+
222
+ `hasFocus` reports keyboard focus (Full Keyboard Access, hardware keyboard),
223
+ not the VoiceOver cursor, which XCTest cannot observe. Confirm VoiceOver focus
224
+ restoration by hand as well.
@@ -251,30 +251,7 @@ parsing, `Date.IntervalFormatStyle`, `URL.FormatStyle` and reusable formatter
251
251
  API design belong to `swift-formatstyle`. Here, only point out locale risks and
252
252
  testing for those, unless the user explicitly asks for an implementation.
253
253
 
254
- ```swift
255
- let flight = Date.now
256
- flight.formatted(date: .long, time: .shortened)
257
- // en_US "June 3, 2026 at 4:05 PM", de_DE "3. Juni 2026 um 16:05"; ja_JP uses year-month-day order
258
- flight.formatted(.dateTime.month(.wide).day().year())
259
- Text(flight, format: .dateTime.weekday().hour().minute())
260
-
261
- let visitors = 1_284_503
262
- visitors.formatted() // "1,284,503" / "1.284.503"
263
- 3.14159.formatted(.number.precision(.fractionLength(2)))
264
- 0.18.formatted(.percent) // "18%"
265
- Decimal(59.9).formatted(.currency(code: "USD"))
266
- Decimal(59.9).formatted(.currency(code: "EUR")) // symbol position follows locale
267
-
268
- Measurement(value: 42, unit: UnitLength.kilometers)
269
- .formatted(.measurement(width: .wide)) // miles in en_US
270
- Measurement(value: 21, unit: UnitTemperature.celsius)
271
- .formatted(.measurement(width: .abbreviated)) // Fahrenheit in en_US
272
-
273
- Duration.seconds(5_025).formatted(.time(pattern: .hourMinuteSecond)) // "1:23:45"
274
- var author = PersonNameComponents(); author.givenName = "Aiko"; author.familyName = "Tanaka"
275
- author.formatted(.name(style: .long)) // order differs en_US vs ja_JP
276
- ["tea", "coffee", "juice"].formatted(.list(type: .and)) // "tea, coffee, and juice"; French joins with "et" and no serial comma
277
- ```
254
+ Code: [Locale-Aware Formatting with FormatStyle](references/code-examples.md#locale-aware-formatting-with-formatstyle).
278
255
 
279
256
  Full coverage of every style is in
280
257
  [references/formatstyle-locale.md](references/formatstyle-locale.md).
@@ -363,3 +340,4 @@ Rules: prefer `.leading` and `.trailing` (they flip) over `.left` and `.right`
363
340
  - [references/formatstyle-locale.md](references/formatstyle-locale.md):
364
341
  FormatStyle per value type, forcing a locale, RTL, `@ScaledMetric` and
365
342
  Accessibility Inspector.
343
+ - [Code examples](references/code-examples.md): open for the full code behind Locale-Aware Formatting with FormatStyle.
@@ -0,0 +1,30 @@
1
+ # iOS Localization Code Examples
2
+
3
+ Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
4
+
5
+ ## Locale-Aware Formatting with FormatStyle
6
+
7
+ ```swift
8
+ let flight = Date.now
9
+ flight.formatted(date: .long, time: .shortened)
10
+ // en_US "June 3, 2026 at 4:05 PM", de_DE "3. Juni 2026 um 16:05"; ja_JP uses year-month-day order
11
+ flight.formatted(.dateTime.month(.wide).day().year())
12
+ Text(flight, format: .dateTime.weekday().hour().minute())
13
+
14
+ let visitors = 1_284_503
15
+ visitors.formatted() // "1,284,503" / "1.284.503"
16
+ 3.14159.formatted(.number.precision(.fractionLength(2)))
17
+ 0.18.formatted(.percent) // "18%"
18
+ Decimal(59.9).formatted(.currency(code: "USD"))
19
+ Decimal(59.9).formatted(.currency(code: "EUR")) // symbol position follows locale
20
+
21
+ Measurement(value: 42, unit: UnitLength.kilometers)
22
+ .formatted(.measurement(width: .wide)) // miles in en_US
23
+ Measurement(value: 21, unit: UnitTemperature.celsius)
24
+ .formatted(.measurement(width: .abbreviated)) // Fahrenheit in en_US
25
+
26
+ Duration.seconds(5_025).formatted(.time(pattern: .hourMinuteSecond)) // "1:23:45"
27
+ var author = PersonNameComponents(); author.givenName = "Aiko"; author.familyName = "Tanaka"
28
+ author.formatted(.name(style: .long)) // order differs en_US vs ja_JP
29
+ ["tea", "coffee", "juice"].formatted(.list(type: .and)) // "tea, coffee, and juice"; French joins with "et" and no serial comma
30
+ ```
@@ -101,42 +101,7 @@ for try await line in stream.lines {
101
101
  Put the client behind a protocol. Tests then substitute a double instead of
102
102
  mocking URLSession.
103
103
 
104
- ```swift
105
- protocol APIClientProtocol: Sendable {
106
- func fetch<Reply: Decodable & Sendable>(_ endpoint: Endpoint, as kind: Reply.Type) async throws -> Reply
107
- func send<Reply: Decodable & Sendable>(_ endpoint: Endpoint, json payload: some Encodable & Sendable,
108
- as kind: Reply.Type) async throws -> Reply
109
- func perform(_ endpoint: Endpoint) async throws
110
- func upload<Reply: Decodable & Sendable>(_ endpoint: Endpoint, bytes: Data,
111
- as kind: Reply.Type) async throws -> Reply
112
- }
113
-
114
- struct Endpoint: Sendable {
115
- enum HTTPMethod: String, Sendable { case get, post, put, patch, delete }
116
-
117
- var path: String
118
- var method: HTTPMethod = .get
119
- var queryItems: [URLQueryItem] = []
120
- var headers: [String: String] = [:]
121
- var body: Data? = nil
122
- var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
123
- var timeoutInterval: TimeInterval = 30
124
-
125
- func urlRequest(relativeTo baseURL: URL) throws -> URLRequest {
126
- guard var parts = URLComponents(url: baseURL.appending(path: path),
127
- resolvingAgainstBaseURL: true) else {
128
- throw NetworkError.invalidURL
129
- }
130
- if !queryItems.isEmpty { parts.queryItems = queryItems }
131
- guard let url = parts.url else { throw NetworkError.invalidURL }
132
- var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval)
133
- request.httpMethod = method.rawValue.uppercased()
134
- request.httpBody = body
135
- request.allHTTPHeaderFields = headers
136
- return request
137
- }
138
- }
139
- ```
104
+ Code: [Protocol-based client](references/code-examples.md#protocol-based-client).
140
105
 
141
106
  A bad URL is a thrown `NetworkError.invalidURL`, never a crash; nothing in
142
107
  this skill force-unwraps.
@@ -269,28 +234,7 @@ Retry inside structured concurrency so cancellation stops the loop. Never
269
234
  retry cancellation or a 4xx, with 429 as the exception; 5xx and transient
270
235
  transport errors are fair game.
271
236
 
272
- ```swift
273
- func withRetry<T: Sendable>(
274
- maxAttempts: Int = 3,
275
- initialDelay: Duration = .seconds(1),
276
- maxDelay: Duration = .seconds(30),
277
- shouldRetry: @Sendable (any Error) -> Bool = isTransient,
278
- operation: @Sendable () async throws -> T
279
- ) async throws -> T {
280
- precondition(maxAttempts > 0, "maxAttempts must be at least 1")
281
- for attempt in 0..<maxAttempts {
282
- try Task.checkCancellation()
283
- do {
284
- return try await operation()
285
- } catch {
286
- guard attempt < maxAttempts - 1, shouldRetry(error) else { throw error }
287
- let capped = min(initialDelay * (1 << attempt), maxDelay)
288
- try await Task.sleep(for: capped + capped * Double.random(in: 0...0.1))
289
- }
290
- }
291
- preconditionFailure("the last attempt always returns or throws")
292
- }
293
- ```
237
+ Code: [Retry with exponential backoff](references/code-examples.md#retry-with-exponential-backoff).
294
238
 
295
239
  There is no sleep after the last attempt: its error is rethrown at once. The
296
240
  `isTransient` predicate is in
@@ -412,3 +356,4 @@ path instead of failing immediately. Implement
412
356
  - [lightweight clients](references/lightweight-clients.md): struct-of-closures client injected through `init` for tests and previews.
413
357
  - [Network framework guide](references/network-framework.md): `NWConnection`, `NWListener`, `NWBrowser`, `NWPathMonitor`, raw TCP, UDP and WebSocket, iOS 26 `NetworkConnection`.
414
358
  - [file storage patterns](references/file-storage-patterns.md): directory choice, `FileProtectionType`, backup exclusion, low-storage handling.
359
+ - [Code examples](references/code-examples.md): open for the full code behind Protocol-based client, Retry with exponential backoff.