@mmerterden/multi-agent-pipeline 20.35.0 → 20.37.0

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 (207) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -5
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +4 -4
  7. package/install/catalog-history.json +1 -1
  8. package/manifest.json +210 -136
  9. package/package.json +5 -1
  10. package/pipeline/commands/multi-agent/SKILL.md +33 -154
  11. package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
  12. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
  13. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
  14. package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
  15. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
  16. package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
  17. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
  18. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
  19. package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
  20. package/pipeline/commands/multi-agent/office-pair/SKILL.md +66 -0
  21. package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
  22. package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
  23. package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
  24. package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
  25. package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
  26. package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
  27. package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
  28. package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
  29. package/pipeline/commands/multi-agent/sync/SKILL.md +38 -167
  30. package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
  31. package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
  32. package/pipeline/contract/CHANGELOG.md +9 -0
  33. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  34. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  35. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  36. package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
  37. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  38. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  39. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  40. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  41. package/pipeline/contract/fixtures/runs-running.json +1 -1
  42. package/pipeline/contract/manifest.json +5 -5
  43. package/pipeline/contract/types/index.d.ts +3 -2
  44. package/pipeline/lib/qr-code.mjs +26 -0
  45. package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
  46. package/pipeline/multi-agent-refs/channels/generator.md +14 -0
  47. package/pipeline/multi-agent-refs/channels/preview.md +13 -0
  48. package/pipeline/multi-agent-refs/channels/reference.md +46 -0
  49. package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
  50. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  51. package/pipeline/multi-agent-refs/design-check/export.md +1 -0
  52. package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
  53. package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
  54. package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
  55. package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
  56. package/pipeline/multi-agent-refs/help/en.md +1 -0
  57. package/pipeline/multi-agent-refs/help/tr.md +1 -0
  58. package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
  59. package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
  60. package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
  61. package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
  62. package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
  63. package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
  64. package/pipeline/multi-agent-refs/resume/details.md +115 -2
  65. package/pipeline/multi-agent-refs/review/inputs.md +33 -1
  66. package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
  67. package/pipeline/multi-agent-refs/review/panel.md +75 -0
  68. package/pipeline/multi-agent-refs/review/valven.md +5 -1
  69. package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
  70. package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
  71. package/pipeline/multi-agent-refs/stack/background.md +72 -0
  72. package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
  73. package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
  74. package/pipeline/multi-agent-refs/sync/office.md +10 -0
  75. package/pipeline/multi-agent-refs/sync/overview.md +61 -0
  76. package/pipeline/multi-agent-refs/sync/repo.md +91 -0
  77. package/pipeline/multi-agent-refs/update/details.md +83 -0
  78. package/pipeline/schemas/runs-index.schema.json +2 -1
  79. package/pipeline/scripts/contract-server.mjs +1 -1
  80. package/pipeline/scripts/office-pair.mjs +40 -9
  81. package/pipeline/scripts/runs-index.mjs +34 -7
  82. package/pipeline/scripts/status-overview.mjs +401 -0
  83. package/pipeline/skills/.skill-manifest.json +69 -65
  84. package/pipeline/skills/.skills-index.json +15 -4
  85. package/pipeline/skills/shared/README.md +4 -3
  86. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +8 -71
  87. package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
  88. package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
  89. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +11 -73
  90. package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
  91. package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
  92. package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
  93. package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
  94. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
  95. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
  96. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
  97. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
  98. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
  99. package/pipeline/skills/shared/core/multi-agent-office-pair/SKILL.md +19 -0
  100. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +1 -1
  101. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
  102. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
  103. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
  104. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
  105. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +75 -219
  106. package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
  107. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
  108. package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
  109. package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
  110. package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
  111. package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
  112. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
  113. package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
  114. package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
  115. package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
  116. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
  117. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
  118. package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
  119. package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
  120. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
  121. package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
  122. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
  123. package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
  124. package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
  125. package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
  126. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
  127. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
  128. package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
  129. package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
  130. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
  131. package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
  132. package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
  133. package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
  134. package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
  135. package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
  136. package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
  137. package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
  138. package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
  139. package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
  140. package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
  141. package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
  142. package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
  143. package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
  144. package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
  145. package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
  146. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
  147. package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
  148. package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
  149. package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
  150. package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
  151. package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
  152. package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
  153. package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
  154. package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
  155. package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
  156. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
  157. package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
  158. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
  159. package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
  160. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
  161. package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
  162. package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
  163. package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
  164. package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
  165. package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
  166. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
  167. package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
  168. package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
  169. package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
  170. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
  171. package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
  172. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
  173. package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
  174. package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
  175. package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
  176. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
  177. package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
  178. package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
  179. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
  180. package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
  181. package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
  182. package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
  183. package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
  184. package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
  185. package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
  186. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
  187. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
  188. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
  189. package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
  190. package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
  191. package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
  192. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
  193. package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
  194. package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
  195. package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
  196. package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
  197. package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
  198. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
  199. package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
  200. package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
  201. package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
  202. package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
  203. package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
  204. package/pipeline/skills/skills-index.md +2 -1
  205. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
  206. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
  207. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
@@ -0,0 +1,70 @@
1
+ # Compose Navigation 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
+ ## Bottom Navigation
6
+
7
+ ```kotlin
8
+ @Serializable
9
+ object HomeRoute
10
+
11
+ @Serializable
12
+ object SearchRoute
13
+
14
+ @Serializable
15
+ object ProfileRoute
16
+
17
+ enum class TopLevelDestination(
18
+ val label: String,
19
+ val icon: ImageVector,
20
+ val route: Any,
21
+ ) {
22
+ HOME("Home", Icons.Default.Home, HomeRoute),
23
+ SEARCH("Search", Icons.Default.Search, SearchRoute),
24
+ PROFILE("Profile", Icons.Default.Person, ProfileRoute),
25
+ }
26
+
27
+ @Composable
28
+ fun MainScreen() {
29
+ val navController = rememberNavController()
30
+ val currentBackStackEntry by navController.currentBackStackEntryAsState()
31
+
32
+ Scaffold(
33
+ bottomBar = {
34
+ NavigationBar {
35
+ TopLevelDestination.entries.forEach { destination ->
36
+ val isSelected = currentBackStackEntry
37
+ ?.destination?.hasRoute(destination.route::class) == true
38
+
39
+ NavigationBarItem(
40
+ selected = isSelected,
41
+ onClick = {
42
+ navController.navigate(destination.route) {
43
+ popUpTo(navController.graph.findStartDestination().id) {
44
+ saveState = true
45
+ }
46
+ launchSingleTop = true
47
+ restoreState = true
48
+ }
49
+ },
50
+ icon = {
51
+ Icon(destination.icon, contentDescription = destination.label)
52
+ },
53
+ label = { Text(destination.label) },
54
+ )
55
+ }
56
+ }
57
+ },
58
+ ) { innerPadding ->
59
+ NavHost(
60
+ navController = navController,
61
+ startDestination = HomeRoute,
62
+ modifier = Modifier.padding(innerPadding),
63
+ ) {
64
+ composable<HomeRoute> { HomeScreen() }
65
+ composable<SearchRoute> { SearchScreen() }
66
+ composable<ProfileRoute> { ProfileScreen() }
67
+ }
68
+ }
69
+ }
70
+ ```
@@ -151,46 +151,11 @@ Batch requests run directly against SQLite. They skip the context, which makes
151
151
  them fast, but they post no context notifications, so **merge their results
152
152
  yourself**.
153
153
 
154
- ```swift
155
- @Sendable func publish(_ result: Any?, as key: String) {
156
- let ids = result as? [NSManagedObjectID] ?? []
157
- let ui = LibraryStore.shared.container.viewContext
158
- NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [key: ids], into: [ui])
159
- }
160
-
161
- let worker = LibraryStore.shared.makeWorker()
162
-
163
- // Insert (iOS 13+)
164
- try await worker.perform {
165
- let insert = NSBatchInsertRequest(entity: Book.entity(), objects: rows.map { ["isbn": $0.isbn, "title": $0.title] })
166
- insert.resultType = .objectIDs
167
- let inserted = try worker.execute(insert) as? NSBatchInsertResult
168
- publish(inserted?.result, as: NSInsertedObjectsKey)
169
- }
170
-
171
- // Delete (iOS 9+)
172
- try await worker.perform {
173
- let stale: NSFetchRequest<NSFetchRequestResult> = Loan.fetchRequest()
174
- stale.predicate = NSPredicate(format: "returnedAt < %@", cutoff as NSDate)
175
- let delete = NSBatchDeleteRequest(fetchRequest: stale)
176
- delete.resultType = .resultTypeObjectIDs
177
- let removed = try worker.execute(delete) as? NSBatchDeleteResult
178
- publish(removed?.result, as: NSDeletedObjectsKey)
179
- }
180
-
181
- // Update (iOS 8+)
182
- try await worker.perform {
183
- let update = NSBatchUpdateRequest(entity: Book.entity())
184
- update.propertiesToUpdate = ["isArchived": true]
185
- update.resultType = .updatedObjectIDsResultType
186
- let changed = try worker.execute(update) as? NSBatchUpdateResult
187
- publish(changed?.result, as: NSUpdatedObjectsKey)
188
- }
189
- ```
190
-
191
154
  A batch delete does not honor the **Deny** delete rule and skips validation
192
155
  beyond the basic delete rules. Check those constraints yourself first.
193
156
 
157
+ Code for this section: [Core Data code patterns](references/core-data-code.md).
158
+
194
159
  ## Persistent history
195
160
 
196
161
  History tracking records store-level changes so the app, its extensions and
@@ -214,58 +179,6 @@ container.persistentStoreDescriptions = [description]
214
179
  3. Merge each transaction into `viewContext` and move the token forward.
215
180
  4. Purge history older than the token.
216
181
 
217
- ```swift
218
- final class HistoryReader: NSObject, @unchecked Sendable {
219
- private let container: NSPersistentContainer
220
- private let worker: NSManagedObjectContext
221
- private let tokenKey: String // one per target: "history.app", "history.widget"
222
- private var lastToken: NSPersistentHistoryToken?
223
-
224
- init(container: NSPersistentContainer, target: String) {
225
- self.container = container
226
- self.worker = container.newBackgroundContext()
227
- self.tokenKey = "history.\(target)"
228
- super.init()
229
- lastToken = Self.loadToken(forKey: tokenKey)
230
- let center = NotificationCenter.default
231
- center.addObserver(self, selector: #selector(storeChanged),
232
- name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
233
- }
234
-
235
- @objc private func storeChanged(_ note: Notification) {
236
- worker.perform { [self] in
237
- let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
238
- let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
239
- guard let transactions = history?.result as? [NSPersistentHistoryTransaction] else { return }
240
- let ui = container.viewContext
241
- for transaction in transactions {
242
- let changes = transaction.objectIDNotification().userInfo ?? [:]
243
- NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [ui])
244
- lastToken = transaction.token
245
- }
246
- Self.saveToken(lastToken, forKey: tokenKey)
247
- if let lastToken {
248
- _ = try? worker.execute(NSPersistentHistoryChangeRequest.deleteHistory(before: lastToken))
249
- }
250
- }
251
- }
252
-
253
- private static var defaults: UserDefaults { UserDefaults(suiteName: "group.app.library") ?? .standard }
254
-
255
- private static func loadToken(forKey key: String) -> NSPersistentHistoryToken? {
256
- defaults.data(forKey: key).flatMap {
257
- try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: $0)
258
- }
259
- }
260
-
261
- private static func saveToken(_ token: NSPersistentHistoryToken?, forKey key: String) {
262
- guard let token else { return }
263
- let encoded = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
264
- defaults.set(encoded, forKey: key)
265
- }
266
- }
267
- ```
268
-
269
182
  `@unchecked Sendable` rests on one guarantee: after `init` (which sets
270
183
  `lastToken` before the observer is registered), `lastToken` is read and written
271
184
  only inside `worker.perform`, and one context runs its `perform` blocks one at a
@@ -281,6 +194,8 @@ Keep one token per target (for example in the shared `UserDefaults` suite) so
281
194
  each process resumes where it stopped after relaunch. Purge only up to the
282
195
  point every reader has consumed.
283
196
 
197
+ Code for this section: [Core Data code patterns](references/core-data-code.md).
198
+
284
199
  ## Staged migration (iOS 17+)
285
200
 
286
201
  `NSStagedMigrationManager` applies an ordered list of stages, each either
@@ -292,40 +207,6 @@ lightweight or custom.
292
207
  - `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
293
208
  reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
294
209
 
295
- ```swift
296
- let v1 = NSManagedObjectModelReference(name: "Library_v1", in: .main, versionChecksum: "k3Qp...v1")
297
- let v2 = NSManagedObjectModelReference(name: "Library_v2", in: .main, versionChecksum: "R8bz...v2")
298
- let v3Checksum = "Yt5m...v3"
299
-
300
- let splitAuthor = NSCustomMigrationStage(migratingFrom: v1, to: v2)
301
- splitAuthor.label = "Split author into first and last name"
302
- splitAuthor.willMigrateHandler = { manager, _ in
303
- guard let store = manager.container else { return }
304
- let context = store.newBackgroundContext()
305
- try context.performAndWait {
306
- let request = NSFetchRequest<NSManagedObject>(entityName: "Book")
307
- for book in try context.fetch(request) {
308
- let full = book.value(forKey: "author") as? String ?? ""
309
- let parts = full.split(separator: " ", maxSplits: 1).map(String.init)
310
- book.setValue(parts.first, forKey: "authorFirst")
311
- book.setValue(parts.count > 1 ? parts[1] : nil, forKey: "authorLast")
312
- }
313
- try context.save()
314
- }
315
- }
316
-
317
- let addIndexes = NSLightweightMigrationStage([v3Checksum])
318
- addIndexes.label = "v2 to v3: add indexes"
319
-
320
- let manager = NSStagedMigrationManager([splitAuthor, addIndexes])
321
- let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
322
- description.setOption(manager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
323
- container.persistentStoreDescriptions = [description]
324
- container.loadPersistentStores { _, error in
325
- if let error { MigrationLog.failed(error) }
326
- }
327
- ```
328
-
329
210
  The option must be on the store description **before** `loadPersistentStores`.
330
211
  Read the checksums from the compiled `.momd` (for example
331
212
  `NSManagedObjectModel.versionChecksum`), never type a model name in their place.
@@ -333,6 +214,8 @@ Read the checksums from the compiled `.momd` (for example
333
214
  Older deployment targets fall back to inferred mapping
334
215
  (`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
335
216
 
217
+ Code for this section: [Core Data code patterns](references/core-data-code.md).
218
+
336
219
  ## Composite attributes (iOS 17+)
337
220
 
338
221
  `NSCompositeAttributeDescription` groups several sub-attributes into one
@@ -353,44 +236,12 @@ from this side:
353
236
 
354
237
  ## Testing with an in-memory store
355
238
 
356
- ```swift
357
- import Testing
358
- import CoreData
359
-
360
- enum TestModel {
361
- nonisolated(unsafe) static let shared: NSManagedObjectModel = { // loaded once, never mutated
362
- let compiled = Bundle(for: LibraryStore.self).url(forResource: "Library", withExtension: "momd")
363
- return compiled.flatMap(NSManagedObjectModel.init(contentsOf:)) ?? NSManagedObjectModel()
364
- }()
365
- }
366
-
367
- func makeMemoryContainer() throws -> NSPersistentContainer {
368
- let container = NSPersistentContainer(name: "Library", managedObjectModel: TestModel.shared)
369
- let memory = NSPersistentStoreDescription()
370
- memory.type = NSInMemoryStoreType
371
- container.persistentStoreDescriptions = [memory]
372
- var failure: Error?
373
- container.loadPersistentStores { _, problem in failure = problem }
374
- if let failure { throw failure }
375
- return container
376
- }
377
-
378
- @Test func savingABookPersistsIt() throws {
379
- let context = try makeMemoryContainer().viewContext
380
- let book = Book(context: context)
381
- book.title = "Dune"
382
- try context.save()
383
-
384
- let fetched = try context.fetch(Book.fetchRequest())
385
- #expect(fetched.count == 1)
386
- #expect(fetched.first?.title == "Dune")
387
- }
388
- ```
389
-
390
239
  Load the model once and pass the same `NSManagedObjectModel` to every test
391
240
  container; loading it per test produces "multiple entity descriptions claim"
392
241
  duplicate-entity warnings.
393
242
 
243
+ Code for this section: [Core Data code patterns](references/core-data-code.md).
244
+
394
245
  ## Common mistakes
395
246
 
396
247
  | Mistake | Fix |
@@ -421,6 +272,7 @@ duplicate-entity warnings.
421
272
 
422
273
  ## References
423
274
 
275
+ - [Core Data code patterns](references/core-data-code.md): open when writing batch requests, consuming persistent history, setting up staged migration or building an in-memory test container.
424
276
  - [Core Data](https://developer.apple.com/documentation/coredata)
425
277
  - [NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer)
426
278
  - [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)
@@ -0,0 +1,221 @@
1
+ # Core Data code patterns
2
+
3
+ <!-- toc -->
4
+ - [Batch requests](#batch-requests)
5
+ - [Staged migration (iOS 17+)](#staged-migration-ios-17)
6
+ - [Testing with an in-memory store](#testing-with-an-in-memory-store)
7
+ <!-- /toc -->
8
+
9
+ Full code for batch requests, persistent history consumption, staged migration and in-memory test containers. SKILL.md keeps the rules; this file holds the worked code.
10
+
11
+ ## Batch requests
12
+
13
+ Batch requests run directly against SQLite. They skip the context, which makes
14
+ them fast, but they post no context notifications, so **merge their results
15
+ yourself**.
16
+
17
+ ```swift
18
+ @Sendable func publish(_ result: Any?, as key: String) {
19
+ let ids = result as? [NSManagedObjectID] ?? []
20
+ let ui = LibraryStore.shared.container.viewContext
21
+ NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [key: ids], into: [ui])
22
+ }
23
+
24
+ let worker = LibraryStore.shared.makeWorker()
25
+
26
+ // Insert (iOS 13+)
27
+ try await worker.perform {
28
+ let insert = NSBatchInsertRequest(entity: Book.entity(), objects: rows.map { ["isbn": $0.isbn, "title": $0.title] })
29
+ insert.resultType = .objectIDs
30
+ let inserted = try worker.execute(insert) as? NSBatchInsertResult
31
+ publish(inserted?.result, as: NSInsertedObjectsKey)
32
+ }
33
+
34
+ // Delete (iOS 9+)
35
+ try await worker.perform {
36
+ let stale: NSFetchRequest<NSFetchRequestResult> = Loan.fetchRequest()
37
+ stale.predicate = NSPredicate(format: "returnedAt < %@", cutoff as NSDate)
38
+ let delete = NSBatchDeleteRequest(fetchRequest: stale)
39
+ delete.resultType = .resultTypeObjectIDs
40
+ let removed = try worker.execute(delete) as? NSBatchDeleteResult
41
+ publish(removed?.result, as: NSDeletedObjectsKey)
42
+ }
43
+
44
+ // Update (iOS 8+)
45
+ try await worker.perform {
46
+ let update = NSBatchUpdateRequest(entity: Book.entity())
47
+ update.propertiesToUpdate = ["isArchived": true]
48
+ update.resultType = .updatedObjectIDsResultType
49
+ let changed = try worker.execute(update) as? NSBatchUpdateResult
50
+ publish(changed?.result, as: NSUpdatedObjectsKey)
51
+ }
52
+ ```
53
+
54
+ A batch delete does not honor the **Deny** delete rule and skips validation
55
+ beyond the basic delete rules. Check those constraints yourself first.
56
+
57
+ ### Consume it
58
+
59
+ 1. Observe `.NSPersistentStoreRemoteChange`, with the container's persistent store coordinator as the object.
60
+ 2. On a background context, fetch transactions after your last token.
61
+ 3. Merge each transaction into `viewContext` and move the token forward.
62
+ 4. Purge history older than the token.
63
+
64
+ ```swift
65
+ final class HistoryReader: NSObject, @unchecked Sendable {
66
+ private let container: NSPersistentContainer
67
+ private let worker: NSManagedObjectContext
68
+ private let tokenKey: String // one per target: "history.app", "history.widget"
69
+ private var lastToken: NSPersistentHistoryToken?
70
+
71
+ init(container: NSPersistentContainer, target: String) {
72
+ self.container = container
73
+ self.worker = container.newBackgroundContext()
74
+ self.tokenKey = "history.\(target)"
75
+ super.init()
76
+ lastToken = Self.loadToken(forKey: tokenKey)
77
+ let center = NotificationCenter.default
78
+ center.addObserver(self, selector: #selector(storeChanged),
79
+ name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
80
+ }
81
+
82
+ @objc private func storeChanged(_ note: Notification) {
83
+ worker.perform { [self] in
84
+ let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
85
+ let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
86
+ guard let transactions = history?.result as? [NSPersistentHistoryTransaction] else { return }
87
+ let ui = container.viewContext
88
+ for transaction in transactions {
89
+ let changes = transaction.objectIDNotification().userInfo ?? [:]
90
+ NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [ui])
91
+ lastToken = transaction.token
92
+ }
93
+ Self.saveToken(lastToken, forKey: tokenKey)
94
+ if let lastToken {
95
+ _ = try? worker.execute(NSPersistentHistoryChangeRequest.deleteHistory(before: lastToken))
96
+ }
97
+ }
98
+ }
99
+
100
+ private static var defaults: UserDefaults { UserDefaults(suiteName: "group.app.library") ?? .standard }
101
+
102
+ private static func loadToken(forKey key: String) -> NSPersistentHistoryToken? {
103
+ defaults.data(forKey: key).flatMap {
104
+ try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: $0)
105
+ }
106
+ }
107
+
108
+ private static func saveToken(_ token: NSPersistentHistoryToken?, forKey key: String) {
109
+ guard let token else { return }
110
+ let encoded = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
111
+ defaults.set(encoded, forKey: key)
112
+ }
113
+ }
114
+ ```
115
+
116
+ `@unchecked Sendable` rests on one guarantee: after `init` (which sets
117
+ `lastToken` before the observer is registered), `lastToken` is read and written
118
+ only inside `worker.perform`, and one context runs its `perform` blocks one at a
119
+ time on its private queue. Creating a new background context per notification
120
+ would break that, because two notifications could then run on two queues at
121
+ once.
122
+
123
+ The static merge call is safe from the worker queue; calling
124
+ `viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
125
+ inside `viewContext.perform` is the equivalent instance form.
126
+
127
+ Keep one token per target (for example in the shared `UserDefaults` suite) so
128
+ each process resumes where it stopped after relaunch. Purge only up to the
129
+ point every reader has consumed.
130
+
131
+ ## Staged migration (iOS 17+)
132
+
133
+ `NSStagedMigrationManager` applies an ordered list of stages, each either
134
+ lightweight or custom.
135
+
136
+ - `NSLightweightMigrationStage` takes the **version checksums** (`[String]`) of
137
+ compiled model versions, not their names, and has a `label`.
138
+ - `NSManagedObjectModelReference(name:in:versionChecksum:)` points to one model version.
139
+ - `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
140
+ reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
141
+
142
+ ```swift
143
+ let v1 = NSManagedObjectModelReference(name: "Library_v1", in: .main, versionChecksum: "k3Qp...v1")
144
+ let v2 = NSManagedObjectModelReference(name: "Library_v2", in: .main, versionChecksum: "R8bz...v2")
145
+ let v3Checksum = "Yt5m...v3"
146
+
147
+ let splitAuthor = NSCustomMigrationStage(migratingFrom: v1, to: v2)
148
+ splitAuthor.label = "Split author into first and last name"
149
+ splitAuthor.willMigrateHandler = { manager, _ in
150
+ guard let store = manager.container else { return }
151
+ let context = store.newBackgroundContext()
152
+ try context.performAndWait {
153
+ let request = NSFetchRequest<NSManagedObject>(entityName: "Book")
154
+ for book in try context.fetch(request) {
155
+ let full = book.value(forKey: "author") as? String ?? ""
156
+ let parts = full.split(separator: " ", maxSplits: 1).map(String.init)
157
+ book.setValue(parts.first, forKey: "authorFirst")
158
+ book.setValue(parts.count > 1 ? parts[1] : nil, forKey: "authorLast")
159
+ }
160
+ try context.save()
161
+ }
162
+ }
163
+
164
+ let addIndexes = NSLightweightMigrationStage([v3Checksum])
165
+ addIndexes.label = "v2 to v3: add indexes"
166
+
167
+ let manager = NSStagedMigrationManager([splitAuthor, addIndexes])
168
+ let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
169
+ description.setOption(manager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
170
+ container.persistentStoreDescriptions = [description]
171
+ container.loadPersistentStores { _, error in
172
+ if let error { MigrationLog.failed(error) }
173
+ }
174
+ ```
175
+
176
+ The option must be on the store description **before** `loadPersistentStores`.
177
+ Read the checksums from the compiled `.momd` (for example
178
+ `NSManagedObjectModel.versionChecksum`), never type a model name in their place.
179
+
180
+ Older deployment targets fall back to inferred mapping
181
+ (`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
182
+
183
+ ## Testing with an in-memory store
184
+
185
+ ```swift
186
+ import Testing
187
+ import CoreData
188
+
189
+ enum TestModel {
190
+ nonisolated(unsafe) static let shared: NSManagedObjectModel = { // loaded once, never mutated
191
+ let compiled = Bundle(for: LibraryStore.self).url(forResource: "Library", withExtension: "momd")
192
+ return compiled.flatMap(NSManagedObjectModel.init(contentsOf:)) ?? NSManagedObjectModel()
193
+ }()
194
+ }
195
+
196
+ func makeMemoryContainer() throws -> NSPersistentContainer {
197
+ let container = NSPersistentContainer(name: "Library", managedObjectModel: TestModel.shared)
198
+ let memory = NSPersistentStoreDescription()
199
+ memory.type = NSInMemoryStoreType
200
+ container.persistentStoreDescriptions = [memory]
201
+ var failure: Error?
202
+ container.loadPersistentStores { _, problem in failure = problem }
203
+ if let failure { throw failure }
204
+ return container
205
+ }
206
+
207
+ @Test func savingABookPersistsIt() throws {
208
+ let context = try makeMemoryContainer().viewContext
209
+ let book = Book(context: context)
210
+ book.title = "Dune"
211
+ try context.save()
212
+
213
+ let fetched = try context.fetch(Book.fetchRequest())
214
+ #expect(fetched.count == 1)
215
+ #expect(fetched.first?.title == "Dune")
216
+ }
217
+ ```
218
+
219
+ Load the model once and pass the same `NSManagedObjectModel` to every test
220
+ container; loading it per test produces "multiple entity descriptions claim"
221
+ duplicate-entity warnings.
@@ -61,48 +61,7 @@ disable the feature instead of starting a session that will fail.
61
61
  The NDEF session is the shortest path when all you need is a URL, some text or
62
62
  a MIME blob.
63
63
 
64
- ```swift
65
- import CoreNFC
66
-
67
- final class BadgeReader: NSObject, NFCNDEFReaderSessionDelegate {
68
- private var session: NFCNDEFReaderSession?
69
-
70
- func scan() {
71
- guard NFCNDEFReaderSession.readingAvailable else { return }
72
- session = NFCNDEFReaderSession(
73
- delegate: self,
74
- queue: nil,
75
- invalidateAfterFirstRead: false
76
- )
77
- session?.alertMessage = "Hold the top of your iPhone near the badge."
78
- session?.begin()
79
- }
80
-
81
- func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {}
82
-
83
- func readerSession(_ reader: NFCNDEFReaderSession, didDetectNDEFs found: [NFCNDEFMessage]) {
84
- for message in found {
85
- for record in message.records {
86
- handle(record)
87
- }
88
- }
89
- }
90
-
91
- func readerSession(_ reader: NFCNDEFReaderSession, didInvalidateWithError error: any Error) {
92
- if let nfcError = error as? NFCReaderError,
93
- nfcError.code == .readerSessionInvalidationErrorFirstNDEFTagRead
94
- || nfcError.code == .readerSessionInvalidationErrorUserCanceled {
95
- // Normal end of a scan, not a failure.
96
- } else {
97
- report(error)
98
- }
99
- self.session = nil
100
- }
101
-
102
- private func handle(_ record: NFCNDEFPayload) {}
103
- private func report(_ error: Error) {}
104
- }
105
- ```
64
+ Code: [Reading NDEF data](references/code-examples.md#reading-ndef-data).
106
65
 
107
66
  The first-read and user-cancel codes are ordinary endings. Only other codes
108
67
  deserve an alert, and the session reference is cleared in every case.
@@ -204,53 +163,7 @@ Payment application identifiers are off limits for this session. For eligible
204
163
  payment use cases in the EU, Apple documents a separate
205
164
  `NFCPaymentTagReaderSession`.
206
165
 
207
- ```swift
208
- final class CardReader: NSObject, NFCTagReaderSessionDelegate {
209
- private var session: NFCTagReaderSession?
210
-
211
- func scan() {
212
- guard NFCTagReaderSession.readingAvailable else { return }
213
- session = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693], delegate: self, queue: nil)
214
- session?.alertMessage = "Hold your transit card near the phone."
215
- session?.begin()
216
- }
217
-
218
- func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}
219
-
220
- func tagReaderSession(_ reader: NFCTagReaderSession, didDetect found: [NFCTag]) {
221
- if found.count > 1 {
222
- retryAfterMultipleTags(reader)
223
- return
224
- }
225
- guard !found.isEmpty else {
226
- reader.restartPolling()
227
- return
228
- }
229
- nonisolated(unsafe) let reader = reader
230
- nonisolated(unsafe) let tags = found
231
- Task {
232
- let tag = tags[0]
233
- do {
234
- try await reader.connect(to: tag)
235
- } catch {
236
- reader.invalidate(errorMessage: error.localizedDescription)
237
- return
238
- }
239
- switch tag {
240
- case .iso7816(let card): await select(card, in: reader)
241
- case .miFare(let chip): await inspect(chip, in: reader)
242
- case .iso15693(let label): await readBlocks(label, in: reader)
243
- case .feliCa(let card): await readSystemCodes(card, in: reader)
244
- @unknown default: reader.invalidate(errorMessage: "This tag type is not handled.")
245
- }
246
- }
247
- }
248
-
249
- func tagReaderSession(_ reader: NFCTagReaderSession, didInvalidateWithError error: any Error) {
250
- session = nil
251
- }
252
- }
253
- ```
166
+ Code: [Native tag access](references/code-examples.md#native-tag-access).
254
167
 
255
168
  The task receives the whole `tags` array rather than a single `NFCTag`: with
256
169
  the enum value itself as the `nonisolated(unsafe)` copy, Swift 6.3 still
@@ -441,3 +354,4 @@ func scene(_ scene: UIScene, continue activity: NSUserActivity) {
441
354
  - keys: [formats entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.nfc.readersession.formats),
442
355
  [ISO 7816 AIDs](https://developer.apple.com/documentation/bundleresources/information-property-list/com.apple.developer.nfc.readersession.iso7816.select-identifiers),
443
356
  [FeliCa system codes](https://developer.apple.com/documentation/bundleresources/information-property-list/com.apple.developer.nfc.readersession.felica.systemcodes)
357
+ - [Code examples](references/code-examples.md): open for the full code behind Reading NDEF data, Native tag access.