@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
@@ -61,36 +61,11 @@ be computed some other way.
61
61
  Write `init(from:)` and `encode(to:)` when a value needs converting on the
62
62
  way in or out.
63
63
 
64
- ```swift
65
- struct Shipment: Codable {
66
- let reference: String
67
- let dispatchedAt: Date
68
- let labels: [String]
69
-
70
- enum CodingKeys: String, CodingKey {
71
- case reference, dispatchedAt = "dispatched_epoch", labels
72
- }
73
-
74
- init(from decoder: any Decoder) throws {
75
- let fields = try decoder.container(keyedBy: CodingKeys.self)
76
- reference = try fields.decode(String.self, forKey: .reference)
77
- let seconds = try fields.decode(Double.self, forKey: .dispatchedAt)
78
- dispatchedAt = Date(timeIntervalSince1970: seconds)
79
- labels = try fields.decodeIfPresent([String].self, forKey: .labels) ?? []
80
- }
81
-
82
- func encode(to encoder: any Encoder) throws {
83
- var fields = encoder.container(keyedBy: CodingKeys.self)
84
- try fields.encode(reference, forKey: .reference)
85
- try fields.encode(dispatchedAt.timeIntervalSince1970, forKey: .dispatchedAt)
86
- try fields.encode(labels, forKey: .labels)
87
- }
88
- }
89
- ```
90
-
91
64
  Doing the conversion here, instead of patching values after decoding, keeps
92
65
  invalid instances from ever existing.
93
66
 
67
+ Code for this section: [Codable recipes](references/codable-recipes.md).
68
+
94
69
  ## Nested and flattened containers
95
70
 
96
71
  JSON often nests what the app wants flat. `nestedContainer(keyedBy:forKey:)`
@@ -125,65 +100,21 @@ For a list whose elements differ by a `kind` field, model the element as an
125
100
  enum with associated values. Read the discriminator first, then the payload it
126
101
  selects.
127
102
 
128
- ```swift
129
- enum FeedItem: Decodable {
130
- case note(String)
131
- case photo(URL)
132
- case poll(options: [String])
133
-
134
- enum Keys: String, CodingKey { case kind, text, url, options }
135
-
136
- init(from decoder: any Decoder) throws {
137
- let item = try decoder.container(keyedBy: Keys.self)
138
- switch try item.decode(String.self, forKey: .kind) {
139
- case "note": self = .note(try item.decode(String.self, forKey: .text))
140
- case "photo": self = .photo(try item.decode(URL.self, forKey: .url))
141
- case "poll": self = .poll(options: try item.decode([String].self, forKey: .options))
142
- case let other:
143
- throw DecodingError.dataCorruptedError(
144
- forKey: .kind, in: item, debugDescription: "No FeedItem kind named \(other)")
145
- }
146
- }
147
- }
148
-
149
- let feed = try JSONDecoder().decode([FeedItem].self, from: payload)
150
- ```
103
+ Code for this section: [Codable recipes](references/codable-recipes.md).
151
104
 
152
105
  ## Date decoding strategies
153
106
 
154
- Set the strategy on the decoder to match what the server sends.
155
-
156
- ```swift
157
- let reader = JSONDecoder()
158
-
159
- reader.dateDecodingStrategy = .iso8601 // "2025-11-02T08:15:00Z"
160
- reader.dateDecodingStrategy = .secondsSince1970 // 1762071300
161
-
162
- let dayFormat = DateFormatter()
163
- dayFormat.dateFormat = "dd.MM.yyyy"
164
- dayFormat.locale = Locale(identifier: "en_US_POSIX")
165
- dayFormat.timeZone = TimeZone(secondsFromGMT: 0)
166
- reader.dateDecodingStrategy = .formatted(dayFormat) // "02.11.2025"
167
- ```
168
-
169
- When one field arrives in more than one shape, decode it by hand:
107
+ Set the strategy on the decoder to match what the server sends: `.iso8601`,
108
+ `.secondsSince1970`, or `.formatted` with an `en_US_POSIX`, UTC `DateFormatter`.
170
109
 
171
- ```swift
172
- reader.dateDecodingStrategy = .custom { decoder in
173
- let raw = try decoder.singleValueContainer()
174
- let text = try raw.decode(String.self)
175
- let withFraction = ISO8601DateFormatter()
176
- withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
177
- if let date = withFraction.date(from: text) ?? ISO8601DateFormatter().date(from: text) {
178
- return date
179
- }
180
- throw DecodingError.dataCorruptedError(in: raw, debugDescription: "Unreadable date: \(text)")
181
- }
182
- ```
110
+ When one field arrives in more than one shape, decode it by hand with a
111
+ `.custom` strategy.
183
112
 
184
113
  Give the encoder the matching strategy, for example
185
114
  `writer.dateEncodingStrategy = .iso8601`, so values round-trip.
186
115
 
116
+ Code for this section: [Codable recipes](references/codable-recipes.md).
117
+
187
118
  ## Data and key strategies
188
119
 
189
120
  ```swift
@@ -211,37 +142,15 @@ ordinary key.
211
142
 
212
143
  By default a single bad element throws, and the whole array is lost. When a
213
144
  feed is known to be unreliable, wrap each element so a failure becomes `nil`
214
- and the container still moves on:
215
-
216
- ```swift
217
- struct TolerantList<Element: Decodable>: Decodable {
218
- let items: [Element]
219
-
220
- private struct Slot: Decodable {
221
- let value: Element?
222
- init(from decoder: any Decoder) throws {
223
- value = try? Element(from: decoder)
224
- }
225
- }
226
-
227
- init(from decoder: any Decoder) throws {
228
- var list = try decoder.unkeyedContainer()
229
- var kept: [Element] = []
230
- while !list.isAtEnd {
231
- if let value = try list.decode(Slot.self).value {
232
- kept.append(value)
233
- }
234
- }
235
- items = kept
236
- }
237
- }
238
- ```
145
+ and the container still moves on (the `Slot` wrapper in the reference).
239
146
 
240
147
  Decoding `Slot` always succeeds, so the unkeyed container always advances.
241
148
  A skip trick that decodes an empty placeholder struct only works when the bad
242
149
  element is a JSON object; a bad string or number would throw again and leave
243
150
  the loop stuck on the same index.
244
151
 
152
+ Code for this section: [Codable recipes](references/codable-recipes.md).
153
+
245
154
  ## Single value containers
246
155
 
247
156
  A wrapper type that should appear in JSON as a bare value uses the single value
@@ -394,52 +303,8 @@ needs something SwiftData cannot store, hand it to the SwiftData skill.
394
303
 
395
304
  `@AppStorage` is for small preferences: `Bool`, numbers, `String`, `URL`,
396
305
  `Data`, or a `RawRepresentable` whose raw value is `Int` or `String`. A small
397
- Codable preference can ride on a JSON string raw value:
398
-
399
- ```swift
400
- struct PlaybackPrefs: Codable {
401
- var speed: Double = 1.0
402
- var skipSilence = false
403
-
404
- init() {}
405
-
406
- enum CodingKeys: String, CodingKey { case speed, skipSilence }
407
-
408
- init(from decoder: any Decoder) throws {
409
- let fields = try decoder.container(keyedBy: CodingKeys.self)
410
- speed = try fields.decodeIfPresent(Double.self, forKey: .speed) ?? 1.0
411
- skipSilence = try fields.decodeIfPresent(Bool.self, forKey: .skipSilence) ?? false
412
- }
413
-
414
- func encode(to encoder: any Encoder) throws {
415
- var fields = encoder.container(keyedBy: CodingKeys.self)
416
- try fields.encode(speed, forKey: .speed)
417
- try fields.encode(skipSilence, forKey: .skipSilence)
418
- }
419
- }
420
-
421
- extension PlaybackPrefs: RawRepresentable {
422
- init?(rawValue: String) {
423
- guard let bytes = rawValue.data(using: .utf8),
424
- let value = try? JSONDecoder().decode(PlaybackPrefs.self, from: bytes) else { return nil }
425
- self = value
426
- }
427
-
428
- var rawValue: String {
429
- guard let bytes = try? JSONEncoder().encode(self),
430
- let text = String(data: bytes, encoding: .utf8) else { return "{}" }
431
- return text
432
- }
433
- }
434
-
435
- struct PlaybackSettingsView: View {
436
- @AppStorage("playbackPrefs") private var prefs = PlaybackPrefs()
437
-
438
- var body: some View {
439
- Toggle("Skip silence", isOn: $prefs.skipSilence)
440
- }
441
- }
442
- ```
306
+ Codable preference can ride on a JSON string raw value (full type in the
307
+ reference).
443
308
 
444
309
  Two details make this bridge safe:
445
310
 
@@ -451,6 +316,8 @@ Two details make this bridge safe:
451
316
  values stored by an older version with fewer keys still decode. Synthesized
452
317
  decoding would ignore the property defaults and fail on `{}`.
453
318
 
319
+ Code for this section: [Codable recipes](references/codable-recipes.md).
320
+
454
321
  ## Common mistakes
455
322
 
456
323
  1. **`decode` for a key that may be absent.** It throws `keyNotFound`. Use
@@ -482,6 +349,7 @@ Two details make this bridge safe:
482
349
 
483
350
  ## References
484
351
 
352
+ - [Codable recipes](references/codable-recipes.md): open when writing a custom `init(from:)`, decoding mixed-type arrays, choosing a date strategy, skipping bad array elements or persisting Codable values in UserDefaults.
485
353
  - [Codable](https://developer.apple.com/documentation/swift/codable)
486
354
  - [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
487
355
  - [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder)
@@ -0,0 +1,207 @@
1
+ # Codable recipes
2
+
3
+ <!-- toc -->
4
+ - [Custom decoding and encoding](#custom-decoding-and-encoding)
5
+ - [Heterogeneous arrays](#heterogeneous-arrays)
6
+ - [Date decoding strategies](#date-decoding-strategies)
7
+ - [Lossy array decoding](#lossy-array-decoding)
8
+ - [Codable with UserDefaults](#codable-with-userdefaults)
9
+ <!-- /toc -->
10
+
11
+ Full code for custom decoding, heterogeneous arrays, date strategies, lossy arrays and storing Codable values in UserDefaults. SKILL.md keeps the rules and when to use each technique.
12
+
13
+ ## Custom decoding and encoding
14
+
15
+ Write `init(from:)` and `encode(to:)` when a value needs converting on the
16
+ way in or out.
17
+
18
+ ```swift
19
+ struct Shipment: Codable {
20
+ let reference: String
21
+ let dispatchedAt: Date
22
+ let labels: [String]
23
+
24
+ enum CodingKeys: String, CodingKey {
25
+ case reference, dispatchedAt = "dispatched_epoch", labels
26
+ }
27
+
28
+ init(from decoder: any Decoder) throws {
29
+ let fields = try decoder.container(keyedBy: CodingKeys.self)
30
+ reference = try fields.decode(String.self, forKey: .reference)
31
+ let seconds = try fields.decode(Double.self, forKey: .dispatchedAt)
32
+ dispatchedAt = Date(timeIntervalSince1970: seconds)
33
+ labels = try fields.decodeIfPresent([String].self, forKey: .labels) ?? []
34
+ }
35
+
36
+ func encode(to encoder: any Encoder) throws {
37
+ var fields = encoder.container(keyedBy: CodingKeys.self)
38
+ try fields.encode(reference, forKey: .reference)
39
+ try fields.encode(dispatchedAt.timeIntervalSince1970, forKey: .dispatchedAt)
40
+ try fields.encode(labels, forKey: .labels)
41
+ }
42
+ }
43
+ ```
44
+
45
+ Doing the conversion here, instead of patching values after decoding, keeps
46
+ invalid instances from ever existing.
47
+
48
+ ## Heterogeneous arrays
49
+
50
+ For a list whose elements differ by a `kind` field, model the element as an
51
+ enum with associated values. Read the discriminator first, then the payload it
52
+ selects.
53
+
54
+ ```swift
55
+ enum FeedItem: Decodable {
56
+ case note(String)
57
+ case photo(URL)
58
+ case poll(options: [String])
59
+
60
+ enum Keys: String, CodingKey { case kind, text, url, options }
61
+
62
+ init(from decoder: any Decoder) throws {
63
+ let item = try decoder.container(keyedBy: Keys.self)
64
+ switch try item.decode(String.self, forKey: .kind) {
65
+ case "note": self = .note(try item.decode(String.self, forKey: .text))
66
+ case "photo": self = .photo(try item.decode(URL.self, forKey: .url))
67
+ case "poll": self = .poll(options: try item.decode([String].self, forKey: .options))
68
+ case let other:
69
+ throw DecodingError.dataCorruptedError(
70
+ forKey: .kind, in: item, debugDescription: "No FeedItem kind named \(other)")
71
+ }
72
+ }
73
+ }
74
+
75
+ let feed = try JSONDecoder().decode([FeedItem].self, from: payload)
76
+ ```
77
+
78
+ ## Date decoding strategies
79
+
80
+ Set the strategy on the decoder to match what the server sends.
81
+
82
+ ```swift
83
+ let reader = JSONDecoder()
84
+
85
+ reader.dateDecodingStrategy = .iso8601 // "2025-11-02T08:15:00Z"
86
+ reader.dateDecodingStrategy = .secondsSince1970 // 1762071300
87
+
88
+ let dayFormat = DateFormatter()
89
+ dayFormat.dateFormat = "dd.MM.yyyy"
90
+ dayFormat.locale = Locale(identifier: "en_US_POSIX")
91
+ dayFormat.timeZone = TimeZone(secondsFromGMT: 0)
92
+ reader.dateDecodingStrategy = .formatted(dayFormat) // "02.11.2025"
93
+ ```
94
+
95
+ When one field arrives in more than one shape, decode it by hand:
96
+
97
+ ```swift
98
+ reader.dateDecodingStrategy = .custom { decoder in
99
+ let raw = try decoder.singleValueContainer()
100
+ let text = try raw.decode(String.self)
101
+ let withFraction = ISO8601DateFormatter()
102
+ withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
103
+ if let date = withFraction.date(from: text) ?? ISO8601DateFormatter().date(from: text) {
104
+ return date
105
+ }
106
+ throw DecodingError.dataCorruptedError(in: raw, debugDescription: "Unreadable date: \(text)")
107
+ }
108
+ ```
109
+
110
+ Give the encoder the matching strategy, for example
111
+ `writer.dateEncodingStrategy = .iso8601`, so values round-trip.
112
+
113
+ ## Lossy array decoding
114
+
115
+ By default a single bad element throws, and the whole array is lost. When a
116
+ feed is known to be unreliable, wrap each element so a failure becomes `nil`
117
+ and the container still moves on:
118
+
119
+ ```swift
120
+ struct TolerantList<Element: Decodable>: Decodable {
121
+ let items: [Element]
122
+
123
+ private struct Slot: Decodable {
124
+ let value: Element?
125
+ init(from decoder: any Decoder) throws {
126
+ value = try? Element(from: decoder)
127
+ }
128
+ }
129
+
130
+ init(from decoder: any Decoder) throws {
131
+ var list = try decoder.unkeyedContainer()
132
+ var kept: [Element] = []
133
+ while !list.isAtEnd {
134
+ if let value = try list.decode(Slot.self).value {
135
+ kept.append(value)
136
+ }
137
+ }
138
+ items = kept
139
+ }
140
+ }
141
+ ```
142
+
143
+ Decoding `Slot` always succeeds, so the unkeyed container always advances.
144
+ A skip trick that decodes an empty placeholder struct only works when the bad
145
+ element is a JSON object; a bad string or number would throw again and leave
146
+ the loop stuck on the same index.
147
+
148
+ ## Codable with UserDefaults
149
+
150
+ `@AppStorage` is for small preferences: `Bool`, numbers, `String`, `URL`,
151
+ `Data`, or a `RawRepresentable` whose raw value is `Int` or `String`. A small
152
+ Codable preference can ride on a JSON string raw value:
153
+
154
+ ```swift
155
+ struct PlaybackPrefs: Codable {
156
+ var speed: Double = 1.0
157
+ var skipSilence = false
158
+
159
+ init() {}
160
+
161
+ enum CodingKeys: String, CodingKey { case speed, skipSilence }
162
+
163
+ init(from decoder: any Decoder) throws {
164
+ let fields = try decoder.container(keyedBy: CodingKeys.self)
165
+ speed = try fields.decodeIfPresent(Double.self, forKey: .speed) ?? 1.0
166
+ skipSilence = try fields.decodeIfPresent(Bool.self, forKey: .skipSilence) ?? false
167
+ }
168
+
169
+ func encode(to encoder: any Encoder) throws {
170
+ var fields = encoder.container(keyedBy: CodingKeys.self)
171
+ try fields.encode(speed, forKey: .speed)
172
+ try fields.encode(skipSilence, forKey: .skipSilence)
173
+ }
174
+ }
175
+
176
+ extension PlaybackPrefs: RawRepresentable {
177
+ init?(rawValue: String) {
178
+ guard let bytes = rawValue.data(using: .utf8),
179
+ let value = try? JSONDecoder().decode(PlaybackPrefs.self, from: bytes) else { return nil }
180
+ self = value
181
+ }
182
+
183
+ var rawValue: String {
184
+ guard let bytes = try? JSONEncoder().encode(self),
185
+ let text = String(data: bytes, encoding: .utf8) else { return "{}" }
186
+ return text
187
+ }
188
+ }
189
+
190
+ struct PlaybackSettingsView: View {
191
+ @AppStorage("playbackPrefs") private var prefs = PlaybackPrefs()
192
+
193
+ var body: some View {
194
+ Toggle("Skip silence", isOn: $prefs.skipSilence)
195
+ }
196
+ }
197
+ ```
198
+
199
+ Two details make this bridge safe:
200
+
201
+ - `encode(to:)` and `init(from:)` are written out. When a type is both
202
+ `RawRepresentable` and Codable, the standard library's `RawRepresentable`
203
+ defaults would otherwise encode `rawValue`, which encodes `self`, which
204
+ recurses until the stack overflows.
205
+ - Decoding uses `decodeIfPresent` with defaults, so the `"{}"` fallback and
206
+ values stored by an older version with fewer keys still decode. Synthesized
207
+ decoding would ignore the property defaults and fail on `{}`.
@@ -82,22 +82,7 @@ SwiftPM `swiftSettings` array, or the Xcode build setting. Use it for app
82
82
  targets, scripts and other executables where most code serves the UI. Leave
83
83
  library targets without it so they stay usable from any isolation.
84
84
 
85
- ```swift
86
- // Target built with Default Actor Isolation = MainActor: no annotations needed.
87
- final class PlaybackQueue {
88
- static let current = PlaybackQueue() // main-actor protected
89
- var upcoming: [Episode] = [] // main-actor protected
90
- }
91
-
92
- final class EpisodeLibrary {
93
- let renderer = WaveformRenderer()
94
- var pinned: [Episode.ID] = []
95
- }
96
-
97
- extension EpisodeLibrary: Archivable { // conformance is main-actor isolated too
98
- func archive() -> Data { Data() }
99
- }
100
- ```
85
+ Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
101
86
 
102
87
  **Async functions stay where they are called (SE-0461).** With
103
88
  `NonisolatedNonsendingByDefault` on, a `nonisolated` async function runs on the
@@ -105,22 +90,7 @@ caller's actor instead of jumping to the global concurrent executor. That
105
90
  behavior is spelled `nonisolated(nonsending)`. Without the feature, Swift 6.0
106
91
  and 6.1 semantics still apply and the function leaves the caller's actor.
107
92
 
108
- ```swift
109
- final class WaveformRenderer {
110
- func render(_ audio: Data) async -> Waveform { Waveform(samples: []) }
111
- }
112
-
113
- @MainActor
114
- final class EpisodeDetailModel {
115
- let renderer = WaveformRenderer()
116
- var waveform: Waveform?
117
-
118
- func open(_ url: URL) async throws {
119
- let (audio, _) = try await URLSession.shared.data(from: url)
120
- waveform = await renderer.render(audio) // stays on the main actor: no race reported
121
- }
122
- }
123
- ```
93
+ Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
124
94
 
125
95
  **`@concurrent` asks for the thread pool on purpose.** A `@concurrent` function
126
96
  always runs on the concurrent pool, which frees the calling actor. To move a
@@ -131,32 +101,11 @@ piece of work off the caller:
131
101
  3. Make the function `async` if it is not already.
132
102
  4. Put `await` at each call site.
133
103
 
134
- ```swift
135
- nonisolated struct TranscriptBuilder {
136
- @concurrent
137
- func build(from audio: Data) async -> Transcript? { Transcript(lines: []) }
138
- }
139
-
140
- // On the main actor:
141
- transcripts[episode.id] = await builder.build(from: audio)
142
- ```
104
+ Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
143
105
 
144
- **Other 6.2 additions**, each covered in
145
- [concurrency-patterns.md](references/concurrency-patterns.md):
146
-
147
- - `Task.immediate` (SE-0472) runs synchronously on the current actor until its
148
- first suspension instead of being enqueued; `Task.immediateDetached` adds
149
- detached semantics. Use it where the enqueue delay is visible to the user,
150
- for example `Task.immediate { await search.apply(query) }`.
151
- - `Observations { }` (SE-0475) turns reads of `@Observable` properties into an
152
- `AsyncSequence` with transactional updates:
153
- `for await elapsed in Observations({ player.elapsed }) { scrubber.move(to: elapsed) }`.
154
- - Isolated conformances let a main-actor type conform to a protocol whose
155
- requirements it can only meet on the main actor. The compiler allows the
156
- conformance only where that isolation holds.
157
- - SE-0473 adds `.epoch` to `ContinuousClock` and `SuspendingClock`, but the
158
- iOS 26.4 SDK does not ship it yet ("value of type 'ContinuousClock' has no
159
- member 'epoch'"); check your SDK before relying on it.
106
+ **Other 6.2 additions** (`Task.immediate`, `Observations { }`, isolated conformances, clock `.epoch`):
107
+ [swift-6-2-additions.md](references/swift-6-2-additions.md), in depth in
108
+ [concurrency-patterns.md](references/concurrency-patterns.md).
160
109
 
161
110
  ## Isolation rules
162
111
 
@@ -199,20 +148,7 @@ transcripts[episode.id] = await builder.build(from: audio)
199
148
  | `Task.immediate { }` | Same as `Task`, starts at once | Latency-sensitive starts (OS 26 runtime) |
200
149
  | `Task.detached { }` | Nothing | Deliberately breaking inheritance; see the rule below |
201
150
 
202
- ```swift
203
- async let profile = api.profile(for: userID)
204
- async let badges = api.badges(for: userID)
205
- let (loadedProfile, loadedBadges) = try await (profile, badges)
206
-
207
- let episodes = try await withThrowingTaskGroup(of: Episode.self) { group in
208
- for id in episodeIDs {
209
- group.addTask { try await api.episode(id) }
210
- }
211
- var collected: [Episode] = []
212
- for try await episode in group { collected.append(episode) }
213
- return collected
214
- }
215
- ```
151
+ Code: [Tasks and structured concurrency](references/code-examples.md#tasks-and-structured-concurrency).
216
152
 
217
153
  SE-0493 lets a `defer` body `await`, but the Swift 6.3.1 compiler in Xcode
218
154
  26.4 still rejects it with "'async' call cannot occur in a defer body". Until
@@ -247,25 +183,7 @@ Cancellation is a request the task must honor itself.
247
183
  An actor runs one piece of code at a time, but every `await` inside it lets
248
184
  other calls in. State read before an `await` may be stale after it.
249
185
 
250
- ```swift
251
- actor TicketCounter {
252
- private var sold = 0
253
- private let audit: AuditLog
254
-
255
- init(audit: AuditLog) { self.audit = audit }
256
-
257
- func sellLosingUpdates() async {
258
- let current = sold
259
- await audit.record(current)
260
- sold = current + 1 // another call may have run during the await
261
- }
262
-
263
- func sell() async {
264
- sold += 1 // read and write with no suspension between them
265
- await audit.record(sold)
266
- }
267
- }
268
- ```
186
+ Code: [Actor reentrancy](references/code-examples.md#actor-reentrancy).
269
187
 
270
188
  ## Streams and continuations
271
189
 
@@ -273,15 +191,7 @@ Bridge APIs that call back many times with `AsyncStream`, and single-shot
273
191
  callbacks with `withCheckedContinuation` or `withCheckedThrowingContinuation`.
274
192
  A continuation is resumed exactly once on every path.
275
193
 
276
- ```swift
277
- func readings(from scale: BluetoothScale) -> AsyncStream<Measurement<UnitMass>> {
278
- AsyncStream { continuation in
279
- let subscription = scale.observe { continuation.yield($0) }
280
- continuation.onTermination = { _ in subscription.cancel() }
281
- scale.startStreaming()
282
- }
283
- }
284
- ```
194
+ Code: [Streams and continuations](references/code-examples.md#streams-and-continuations).
285
195
 
286
196
  Delegate bridges, cancellation-aware continuations and the full GCD mapping are
287
197
  in [bridging-interop.md](references/bridging-interop.md).
@@ -381,3 +291,6 @@ second choices, each with a named reason.
381
291
  adoption order, Thread Sanitizer.
382
292
  - [async-algorithms.md](references/async-algorithms.md): the
383
293
  swift-async-algorithms package.
294
+ - [swift-6-2-additions.md](references/swift-6-2-additions.md): `Task.immediate`,
295
+ `Observations { }`, isolated conformances and the clock `.epoch` status.
296
+ - [Code examples](references/code-examples.md): open for the full code behind Approachable concurrency in Swift 6.2, Tasks and structured concurrency, Actor reentrancy, Streams and continuations.
@@ -0,0 +1,100 @@
1
+ # Swift Concurrency 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
+ ## Approachable concurrency in Swift 6.2
6
+
7
+ ```swift
8
+ // Target built with Default Actor Isolation = MainActor: no annotations needed.
9
+ final class PlaybackQueue {
10
+ static let current = PlaybackQueue() // main-actor protected
11
+ var upcoming: [Episode] = [] // main-actor protected
12
+ }
13
+
14
+ final class EpisodeLibrary {
15
+ let renderer = WaveformRenderer()
16
+ var pinned: [Episode.ID] = []
17
+ }
18
+
19
+ extension EpisodeLibrary: Archivable { // conformance is main-actor isolated too
20
+ func archive() -> Data { Data() }
21
+ }
22
+ ```
23
+
24
+ ```swift
25
+ final class WaveformRenderer {
26
+ func render(_ audio: Data) async -> Waveform { Waveform(samples: []) }
27
+ }
28
+
29
+ @MainActor
30
+ final class EpisodeDetailModel {
31
+ let renderer = WaveformRenderer()
32
+ var waveform: Waveform?
33
+
34
+ func open(_ url: URL) async throws {
35
+ let (audio, _) = try await URLSession.shared.data(from: url)
36
+ waveform = await renderer.render(audio) // stays on the main actor: no race reported
37
+ }
38
+ }
39
+ ```
40
+
41
+ ```swift
42
+ nonisolated struct TranscriptBuilder {
43
+ @concurrent
44
+ func build(from audio: Data) async -> Transcript? { Transcript(lines: []) }
45
+ }
46
+
47
+ // On the main actor:
48
+ transcripts[episode.id] = await builder.build(from: audio)
49
+ ```
50
+
51
+ ## Tasks and structured concurrency
52
+
53
+ ```swift
54
+ async let profile = api.profile(for: userID)
55
+ async let badges = api.badges(for: userID)
56
+ let (loadedProfile, loadedBadges) = try await (profile, badges)
57
+
58
+ let episodes = try await withThrowingTaskGroup(of: Episode.self) { group in
59
+ for id in episodeIDs {
60
+ group.addTask { try await api.episode(id) }
61
+ }
62
+ var collected: [Episode] = []
63
+ for try await episode in group { collected.append(episode) }
64
+ return collected
65
+ }
66
+ ```
67
+
68
+ ## Actor reentrancy
69
+
70
+ ```swift
71
+ actor TicketCounter {
72
+ private var sold = 0
73
+ private let audit: AuditLog
74
+
75
+ init(audit: AuditLog) { self.audit = audit }
76
+
77
+ func sellLosingUpdates() async {
78
+ let current = sold
79
+ await audit.record(current)
80
+ sold = current + 1 // another call may have run during the await
81
+ }
82
+
83
+ func sell() async {
84
+ sold += 1 // read and write with no suspension between them
85
+ await audit.record(sold)
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Streams and continuations
91
+
92
+ ```swift
93
+ func readings(from scale: BluetoothScale) -> AsyncStream<Measurement<UnitMass>> {
94
+ AsyncStream { continuation in
95
+ let subscription = scale.observe { continuation.yield($0) }
96
+ continuation.onTermination = { _ in subscription.cancel() }
97
+ scale.startStreaming()
98
+ }
99
+ }
100
+ ```