@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
@@ -43,26 +43,7 @@ let sharedDB = container.sharedCloudDatabase
43
43
  A `CKRecord` is a typed dictionary. One record can hold up to 1 MB, not
44
44
  counting the files behind any `CKAsset` fields.
45
45
 
46
- ```swift
47
- func plant(named name: String, in db: CKDatabase) async throws -> CKRecord {
48
- let plant = CKRecord(recordType: "Plant")
49
- plant["name"] = name as CKRecordValue
50
- plant["plantedOn"] = Date.now as CKRecordValue
51
- plant["tags"] = ["herb", "sunny"] as CKRecordValue
52
- return try await db.save(plant)
53
- }
54
-
55
- func rename(_ recordName: String, to newName: String, in db: CKDatabase) async throws {
56
- let id = CKRecord.ID(recordName: recordName)
57
- let existing = try await db.record(for: id) // always fetch before editing
58
- existing["name"] = newName as CKRecordValue
59
- _ = try await db.save(existing)
60
- }
61
-
62
- func uproot(_ recordName: String, in db: CKDatabase) async throws {
63
- _ = try await db.deleteRecord(withID: CKRecord.ID(recordName: recordName))
64
- }
65
- ```
46
+ Code for this section: [CloudKit code](references/cloudkit-code.md).
66
47
 
67
48
  ### Custom zones
68
49
 
@@ -120,38 +101,7 @@ With CloudKit enabled, Xcode manages the push entitlement; no separate App ID
120
101
  push setup is needed. Silent pushes still require **Background Modes >
121
102
  Remote notifications**.
122
103
 
123
- ```swift
124
- let newPlants = CKQuerySubscription(
125
- recordType: "Plant",
126
- predicate: NSPredicate(value: true),
127
- subscriptionID: "plant-changes",
128
- options: [.firesOnRecordCreation, .firesOnRecordUpdate]
129
- )
130
- let info = CKSubscription.NotificationInfo()
131
- info.shouldSendContentAvailable = true // silent
132
- newPlants.notificationInfo = info
133
- _ = try await publicDB.save(newPlants)
134
-
135
- let everything = CKDatabaseSubscription(subscriptionID: "private-db") // any change in the DB
136
- let oneZone = CKRecordZoneSubscription(zoneID: beds, subscriptionID: "beds") // changes in one zone
137
- ```
138
-
139
- ```swift
140
- import UIKit
141
-
142
- // UIApplicationDelegate
143
- func application(_ application: UIApplication,
144
- didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
145
- guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo),
146
- note.subscriptionID == "private-db" else { return .noData }
147
- do {
148
- try await GardenSync.shared.pullNow() // CKSyncEngine or CKFetchRecordZoneChangesOperation
149
- return .newData
150
- } catch {
151
- return .failed
152
- }
153
- }
154
- ```
104
+ Code for this section: [CloudKit code](references/cloudkit-code.md).
155
105
 
156
106
  ## CKSyncEngine (iOS 17+)
157
107
 
@@ -164,65 +114,14 @@ Its automatic timing is not predictable. It needs the CloudKit capability plus
164
114
  Remote notifications, and works only with private and shared databases, never
165
115
  public.
166
116
 
167
- ```swift
168
- actor GardenSync: CKSyncEngineDelegate {
169
- static let shared = GardenSync()
170
- private var engine: CKSyncEngine?
171
-
172
- func start() {
173
- guard engine == nil else { return }
174
- let config = CKSyncEngine.Configuration(
175
- database: CKContainer.default().privateCloudDatabase,
176
- stateSerialization: StateFile.load(), // CKSyncEngine.State.Serialization?
177
- delegate: self
178
- )
179
- engine = CKSyncEngine(config)
180
- }
181
-
182
- func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
183
- switch event {
184
- case .stateUpdate(let update):
185
- StateFile.save(update.stateSerialization) // resume from the right token next launch
186
- case .accountChange(let change):
187
- await LocalStore.shared.handle(change)
188
- case .fetchedRecordZoneChanges(let fetched):
189
- for mod in fetched.modifications { await LocalStore.shared.upsert(mod.record) }
190
- for gone in fetched.deletions { await LocalStore.shared.remove(gone.recordID) }
191
- case .sentRecordZoneChanges(let sent):
192
- for saved in sent.savedRecords { await LocalStore.shared.markSynced(saved) }
193
- for failure in sent.failedRecordSaves { await resolve(failure, engine: syncEngine) }
194
- default:
195
- break
196
- }
197
- }
198
-
199
- func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
200
- syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
201
- let scope = context.options.scope
202
- let pending = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) }
203
- return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { id in
204
- await LocalStore.shared.record(for: id)
205
- }
206
- }
207
-
208
- func queueUpload(of id: CKRecord.ID) {
209
- engine?.state.add(pendingRecordZoneChanges: [.saveRecord(id)])
210
- }
211
-
212
- func pullNow() async throws {
213
- guard let engine else { return }
214
- try await engine.fetchChanges() // pull-to-refresh or push arrival
215
- try await engine.sendChanges()
216
- }
217
- }
218
- ```
219
-
220
117
  `context.options.scope` says which zones (for example `.zoneIDs([...])`) or
221
118
  records this send covers; filter the pending changes to it rather than
222
119
  sending everything. Both delegate methods are `async`. Persist
223
120
  `stateSerialization` on every `.stateUpdate`; without it the engine starts
224
121
  over on the next launch.
225
122
 
123
+ Code for this section: [CloudKit code](references/cloudkit-code.md).
124
+
226
125
  ## SwiftData with CloudKit
227
126
 
228
127
  `ModelConfiguration` can sync a SwiftData store to CloudKit. Any
@@ -317,28 +216,6 @@ coordinated read of a ubiquitous item waits for its contents to download
317
216
  unless `.immediatelyAvailableMetadataOnly` is passed. Coordination blocks the
318
217
  calling thread, so keep it off the main actor.
319
218
 
320
- ```swift
321
- nonisolated func readCoordinated(_ url: URL) throws -> Data {
322
- var coordinationError: NSError?
323
- var result: Result<Data, any Error> = .failure(CocoaError(.fileReadUnknown))
324
- NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordinationError) { safeURL in
325
- result = Result { try Data(contentsOf: safeURL) }
326
- }
327
- if let coordinationError { throw coordinationError }
328
- return try result.get()
329
- }
330
-
331
- nonisolated func writeCoordinated(_ data: Data, to url: URL) throws {
332
- var coordinationError: NSError?
333
- var writeError: (any Error)?
334
- NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &coordinationError) { safeURL in
335
- do { try data.write(to: safeURL, options: .atomic) } catch { writeError = error }
336
- }
337
- if let coordinationError { throw coordinationError }
338
- if let writeError { throw writeError }
339
- }
340
- ```
341
-
342
219
  An object that keeps a file open (an editor, a live preview) adopts
343
220
  `NSFilePresenter` so it hears about changes, moves and deletions that iCloud
344
221
  makes: implement `presentedItemURL`, return a private `OperationQueue` from
@@ -349,6 +226,8 @@ with `removeFilePresenter(_:)`. Create the presenter's own coordinators with
349
226
  `UIDocument` already does all of this, so a document-based app gets
350
227
  coordination by subclassing it.
351
228
 
229
+ Code for this section: [CloudKit code](references/cloudkit-code.md).
230
+
352
231
  ## Account status
353
232
 
354
233
  Check the account before any sync, and listen for `.CKAccountChanged`.
@@ -389,25 +268,7 @@ A `.serverRecordChanged` error carries `ancestorRecord`, `clientRecord` and
389
268
  `serverRecord`. Always merge **into `serverRecord`**, because it holds the
390
269
  current change tag; saving the client copy just fails again.
391
270
 
392
- ```swift
393
- func merged(_ error: CKError) -> CKRecord? {
394
- guard let server = error.serverRecord, let mine = error.clientRecord else { return nil }
395
- let base = error.ancestorRecord
396
- for key in mine.changedKeys() {
397
- let serverValue = server[key] as? NSObject
398
- let baseValue = base?[key] as? NSObject
399
- let myValue = mine[key] as? NSObject
400
- if serverValue == baseValue {
401
- server[key] = mine[key] // only I changed it
402
- } else if myValue == baseValue {
403
- continue // only the server changed it
404
- } else {
405
- server[key] = resolveBothChanged(key, mine: mine[key], theirs: server[key])
406
- }
407
- }
408
- return server // save this one
409
- }
410
- ```
271
+ Code for this section: [CloudKit code](references/cloudkit-code.md).
411
272
 
412
273
  ## Common mistakes
413
274
 
@@ -441,6 +302,7 @@ func merged(_ error: CKError) -> CKRecord? {
441
302
 
442
303
  ## References
443
304
 
305
+ - [CloudKit code](references/cloudkit-code.md): open for the code behind records, subscriptions, CKSyncEngine, coordinated file access and conflict resolution.
444
306
  - [CloudKit patterns](references/cloudkit-patterns.md) - incremental fetches, tokens, sharing, zones, assets, batches, QoS, encryption, Dashboard
445
307
  - [CloudKit](https://developer.apple.com/documentation/cloudkit)
446
308
  - [CKContainer](https://developer.apple.com/documentation/cloudkit/ckcontainer)
@@ -0,0 +1,214 @@
1
+ # CloudKit code
2
+
3
+ <!-- toc -->
4
+ - [Records](#records)
5
+ - [Subscriptions](#subscriptions)
6
+ - [CKSyncEngine (iOS 17+)](#cksyncengine-ios-17)
7
+ - [Conflict resolution](#conflict-resolution)
8
+ <!-- /toc -->
9
+
10
+ Code for records, subscriptions, CKSyncEngine, coordinated document access and conflict resolution. SKILL.md keeps the rules and decision guidance.
11
+
12
+ ## Records
13
+
14
+ A `CKRecord` is a typed dictionary. One record can hold up to 1 MB, not
15
+ counting the files behind any `CKAsset` fields.
16
+
17
+ ```swift
18
+ func plant(named name: String, in db: CKDatabase) async throws -> CKRecord {
19
+ let plant = CKRecord(recordType: "Plant")
20
+ plant["name"] = name as CKRecordValue
21
+ plant["plantedOn"] = Date.now as CKRecordValue
22
+ plant["tags"] = ["herb", "sunny"] as CKRecordValue
23
+ return try await db.save(plant)
24
+ }
25
+
26
+ func rename(_ recordName: String, to newName: String, in db: CKDatabase) async throws {
27
+ let id = CKRecord.ID(recordName: recordName)
28
+ let existing = try await db.record(for: id) // always fetch before editing
29
+ existing["name"] = newName as CKRecordValue
30
+ _ = try await db.save(existing)
31
+ }
32
+
33
+ func uproot(_ recordName: String, in db: CKDatabase) async throws {
34
+ _ = try await db.deleteRecord(withID: CKRecord.ID(recordName: recordName))
35
+ }
36
+ ```
37
+
38
+ ## Subscriptions
39
+
40
+ A subscription makes CloudKit push a notification when server data changes.
41
+ With CloudKit enabled, Xcode manages the push entitlement; no separate App ID
42
+ push setup is needed. Silent pushes still require **Background Modes >
43
+ Remote notifications**.
44
+
45
+ ```swift
46
+ let newPlants = CKQuerySubscription(
47
+ recordType: "Plant",
48
+ predicate: NSPredicate(value: true),
49
+ subscriptionID: "plant-changes",
50
+ options: [.firesOnRecordCreation, .firesOnRecordUpdate]
51
+ )
52
+ let info = CKSubscription.NotificationInfo()
53
+ info.shouldSendContentAvailable = true // silent
54
+ newPlants.notificationInfo = info
55
+ _ = try await publicDB.save(newPlants)
56
+
57
+ let everything = CKDatabaseSubscription(subscriptionID: "private-db") // any change in the DB
58
+ let oneZone = CKRecordZoneSubscription(zoneID: beds, subscriptionID: "beds") // changes in one zone
59
+ ```
60
+
61
+ ```swift
62
+ import UIKit
63
+
64
+ // UIApplicationDelegate
65
+ func application(_ application: UIApplication,
66
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
67
+ guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo),
68
+ note.subscriptionID == "private-db" else { return .noData }
69
+ do {
70
+ try await GardenSync.shared.pullNow() // CKSyncEngine or CKFetchRecordZoneChangesOperation
71
+ return .newData
72
+ } catch {
73
+ return .failed
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## CKSyncEngine (iOS 17+)
79
+
80
+ For custom model data, `CKSyncEngine` is the recommended route. It schedules
81
+ work, retries transient failures, keeps change tokens and manages the
82
+ database subscription for you. It does **not** resolve app-level save
83
+ failures: a `CKError.serverRecordChanged` inside
84
+ `sentRecordZoneChanges.failedRecordSaves` needs your merge and a reschedule.
85
+ Its automatic timing is not predictable. It needs the CloudKit capability plus
86
+ Remote notifications, and works only with private and shared databases, never
87
+ public.
88
+
89
+ ```swift
90
+ actor GardenSync: CKSyncEngineDelegate {
91
+ static let shared = GardenSync()
92
+ private var engine: CKSyncEngine?
93
+
94
+ func start() {
95
+ guard engine == nil else { return }
96
+ let config = CKSyncEngine.Configuration(
97
+ database: CKContainer.default().privateCloudDatabase,
98
+ stateSerialization: StateFile.load(), // CKSyncEngine.State.Serialization?
99
+ delegate: self
100
+ )
101
+ engine = CKSyncEngine(config)
102
+ }
103
+
104
+ func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
105
+ switch event {
106
+ case .stateUpdate(let update):
107
+ StateFile.save(update.stateSerialization) // resume from the right token next launch
108
+ case .accountChange(let change):
109
+ await LocalStore.shared.handle(change)
110
+ case .fetchedRecordZoneChanges(let fetched):
111
+ for mod in fetched.modifications { await LocalStore.shared.upsert(mod.record) }
112
+ for gone in fetched.deletions { await LocalStore.shared.remove(gone.recordID) }
113
+ case .sentRecordZoneChanges(let sent):
114
+ for saved in sent.savedRecords { await LocalStore.shared.markSynced(saved) }
115
+ for failure in sent.failedRecordSaves { await resolve(failure, engine: syncEngine) }
116
+ default:
117
+ break
118
+ }
119
+ }
120
+
121
+ func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
122
+ syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
123
+ let scope = context.options.scope
124
+ let pending = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) }
125
+ return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { id in
126
+ await LocalStore.shared.record(for: id)
127
+ }
128
+ }
129
+
130
+ func queueUpload(of id: CKRecord.ID) {
131
+ engine?.state.add(pendingRecordZoneChanges: [.saveRecord(id)])
132
+ }
133
+
134
+ func pullNow() async throws {
135
+ guard let engine else { return }
136
+ try await engine.fetchChanges() // pull-to-refresh or push arrival
137
+ try await engine.sendChanges()
138
+ }
139
+ }
140
+ ```
141
+
142
+ `context.options.scope` says which zones (for example `.zoneIDs([...])`) or
143
+ records this send covers; filter the pending changes to it rather than
144
+ sending everything. Both delegate methods are `async`. Persist
145
+ `stateSerialization` on every `.stateUpdate`; without it the engine starts
146
+ over on the next launch.
147
+
148
+ ### Coordinated reads and writes
149
+
150
+ The iCloud daemon reads and writes files in the ubiquity container at the same
151
+ time as the app, so every direct access to a ubiquitous file goes through
152
+ `NSFileCoordinator`. Do the file work only inside the accessor block and only
153
+ with the URL it passes in, which can differ from the one requested. A
154
+ coordinated read of a ubiquitous item waits for its contents to download
155
+ unless `.immediatelyAvailableMetadataOnly` is passed. Coordination blocks the
156
+ calling thread, so keep it off the main actor.
157
+
158
+ ```swift
159
+ nonisolated func readCoordinated(_ url: URL) throws -> Data {
160
+ var coordinationError: NSError?
161
+ var result: Result<Data, any Error> = .failure(CocoaError(.fileReadUnknown))
162
+ NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordinationError) { safeURL in
163
+ result = Result { try Data(contentsOf: safeURL) }
164
+ }
165
+ if let coordinationError { throw coordinationError }
166
+ return try result.get()
167
+ }
168
+
169
+ nonisolated func writeCoordinated(_ data: Data, to url: URL) throws {
170
+ var coordinationError: NSError?
171
+ var writeError: (any Error)?
172
+ NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &coordinationError) { safeURL in
173
+ do { try data.write(to: safeURL, options: .atomic) } catch { writeError = error }
174
+ }
175
+ if let coordinationError { throw coordinationError }
176
+ if let writeError { throw writeError }
177
+ }
178
+ ```
179
+
180
+ An object that keeps a file open (an editor, a live preview) adopts
181
+ `NSFilePresenter` so it hears about changes, moves and deletions that iCloud
182
+ makes: implement `presentedItemURL`, return a private `OperationQueue` from
183
+ `presentedItemOperationQueue` (the main queue invites deadlocks), react in
184
+ `presentedItemDidChange()`, and pair `NSFileCoordinator.addFilePresenter(_:)`
185
+ with `removeFilePresenter(_:)`. Create the presenter's own coordinators with
186
+ `NSFileCoordinator(filePresenter:)` so it is not told about its own writes.
187
+ `UIDocument` already does all of this, so a document-based app gets
188
+ coordination by subclassing it.
189
+
190
+ ## Conflict resolution
191
+
192
+ A `.serverRecordChanged` error carries `ancestorRecord`, `clientRecord` and
193
+ `serverRecord`. Always merge **into `serverRecord`**, because it holds the
194
+ current change tag; saving the client copy just fails again.
195
+
196
+ ```swift
197
+ func merged(_ error: CKError) -> CKRecord? {
198
+ guard let server = error.serverRecord, let mine = error.clientRecord else { return nil }
199
+ let base = error.ancestorRecord
200
+ for key in mine.changedKeys() {
201
+ let serverValue = server[key] as? NSObject
202
+ let baseValue = base?[key] as? NSObject
203
+ let myValue = mine[key] as? NSObject
204
+ if serverValue == baseValue {
205
+ server[key] = mine[key] // only I changed it
206
+ } else if myValue == baseValue {
207
+ continue // only the server changed it
208
+ } else {
209
+ server[key] = resolveBothChanged(key, mine: mine[key], theirs: server[key])
210
+ }
211
+ }
212
+ return server // save this one
213
+ }
214
+ ```
@@ -35,43 +35,7 @@ Material 3 (androidx.compose.material3).
35
35
  Scaffold provides the standard Material 3 layout structure with slots for
36
36
  top bar, bottom bar, FAB, snackbar, and content.
37
37
 
38
- ```kotlin
39
- @OptIn(ExperimentalMaterial3Api::class)
40
- @Composable
41
- fun HomeScreen(
42
- onNavigateToSettings: () -> Unit,
43
- viewModel: HomeViewModel = hiltViewModel(),
44
- ) {
45
- val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
46
- val snackbarHostState = remember { SnackbarHostState() }
47
-
48
- Scaffold(
49
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
50
- topBar = {
51
- TopAppBar(
52
- title = { Text("Home") },
53
- actions = {
54
- IconButton(onClick = onNavigateToSettings) {
55
- Icon(Icons.Default.Settings, contentDescription = "Settings")
56
- }
57
- },
58
- scrollBehavior = scrollBehavior,
59
- )
60
- },
61
- snackbarHost = { SnackbarHost(snackbarHostState) },
62
- floatingActionButton = {
63
- FloatingActionButton(onClick = { /* action */ }) {
64
- Icon(Icons.Default.Add, contentDescription = "Add")
65
- }
66
- },
67
- ) { innerPadding ->
68
- HomeContent(
69
- modifier = Modifier.padding(innerPadding),
70
- viewModel = viewModel,
71
- )
72
- }
73
- }
74
- ```
38
+ Code: [Scaffold and TopAppBar](references/code-examples.md#scaffold-and-topappbar).
75
39
 
76
40
  ### TopAppBar Variants
77
41
 
@@ -171,49 +135,7 @@ fun ScreenWithSnackbar() {
171
135
  Design custom composables with slot parameters (lambdas returning `@Composable`
172
136
  content). This follows the Material 3 pattern used by `Scaffold`, `Card`, etc.
173
137
 
174
- ```kotlin
175
- @Composable
176
- fun InfoCard(
177
- modifier: Modifier = Modifier,
178
- icon: @Composable () -> Unit,
179
- title: @Composable () -> Unit,
180
- subtitle: @Composable (() -> Unit)? = null,
181
- actions: @Composable RowScope.() -> Unit = {},
182
- ) {
183
- Card(modifier = modifier) {
184
- Row(
185
- modifier = Modifier.padding(16.dp),
186
- verticalAlignment = Alignment.CenterVertically,
187
- ) {
188
- icon()
189
- Spacer(modifier = Modifier.width(16.dp))
190
- Column(modifier = Modifier.weight(1f)) {
191
- ProvideTextStyle(MaterialTheme.typography.titleMedium) {
192
- title()
193
- }
194
- subtitle?.let {
195
- ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
196
- it()
197
- }
198
- }
199
- }
200
- Row(content = actions)
201
- }
202
- }
203
- }
204
-
205
- // Usage
206
- InfoCard(
207
- icon = { Icon(Icons.Default.ShoppingCart, contentDescription = null) },
208
- title = { Text("London to New York") },
209
- subtitle = { Text("Order AB1234") },
210
- actions = {
211
- IconButton(onClick = { /* bookmark */ }) {
212
- Icon(Icons.Default.BookmarkBorder, contentDescription = "Bookmark")
213
- }
214
- },
215
- )
216
- ```
138
+ Code: [Slot-Based API Pattern](references/code-examples.md#slot-based-api-pattern).
217
139
 
218
140
  ### Slot Design Rules
219
141
  - First parameter: `modifier: Modifier = Modifier`
@@ -489,3 +411,7 @@ Box(
489
411
  - [ ] Rich tokens exposed via `CompositionLocalProvider` and read through a `@ReadOnlyComposable` `Theme.colors`/`Theme.typography` accessor
490
412
  - [ ] M3 `light`/`darkColorScheme` derived from the tokens so stock Material components inherit brand colors
491
413
  - [ ] Catalog KSP processor gated behind a build flag, annotation kept universal
414
+
415
+ ## References
416
+
417
+ - [Code examples](references/code-examples.md): open for the full code behind Scaffold and TopAppBar, Slot-Based API Pattern.
@@ -0,0 +1,89 @@
1
+ # Compose Components 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
+ ## Scaffold and TopAppBar
6
+
7
+ ```kotlin
8
+ @OptIn(ExperimentalMaterial3Api::class)
9
+ @Composable
10
+ fun HomeScreen(
11
+ onNavigateToSettings: () -> Unit,
12
+ viewModel: HomeViewModel = hiltViewModel(),
13
+ ) {
14
+ val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
15
+ val snackbarHostState = remember { SnackbarHostState() }
16
+
17
+ Scaffold(
18
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
19
+ topBar = {
20
+ TopAppBar(
21
+ title = { Text("Home") },
22
+ actions = {
23
+ IconButton(onClick = onNavigateToSettings) {
24
+ Icon(Icons.Default.Settings, contentDescription = "Settings")
25
+ }
26
+ },
27
+ scrollBehavior = scrollBehavior,
28
+ )
29
+ },
30
+ snackbarHost = { SnackbarHost(snackbarHostState) },
31
+ floatingActionButton = {
32
+ FloatingActionButton(onClick = { /* action */ }) {
33
+ Icon(Icons.Default.Add, contentDescription = "Add")
34
+ }
35
+ },
36
+ ) { innerPadding ->
37
+ HomeContent(
38
+ modifier = Modifier.padding(innerPadding),
39
+ viewModel = viewModel,
40
+ )
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Slot-Based API Pattern
46
+
47
+ ```kotlin
48
+ @Composable
49
+ fun InfoCard(
50
+ modifier: Modifier = Modifier,
51
+ icon: @Composable () -> Unit,
52
+ title: @Composable () -> Unit,
53
+ subtitle: @Composable (() -> Unit)? = null,
54
+ actions: @Composable RowScope.() -> Unit = {},
55
+ ) {
56
+ Card(modifier = modifier) {
57
+ Row(
58
+ modifier = Modifier.padding(16.dp),
59
+ verticalAlignment = Alignment.CenterVertically,
60
+ ) {
61
+ icon()
62
+ Spacer(modifier = Modifier.width(16.dp))
63
+ Column(modifier = Modifier.weight(1f)) {
64
+ ProvideTextStyle(MaterialTheme.typography.titleMedium) {
65
+ title()
66
+ }
67
+ subtitle?.let {
68
+ ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
69
+ it()
70
+ }
71
+ }
72
+ }
73
+ Row(content = actions)
74
+ }
75
+ }
76
+ }
77
+
78
+ // Usage
79
+ InfoCard(
80
+ icon = { Icon(Icons.Default.ShoppingCart, contentDescription = null) },
81
+ title = { Text("London to New York") },
82
+ subtitle = { Text("Order AB1234") },
83
+ actions = {
84
+ IconButton(onClick = { /* bookmark */ }) {
85
+ Icon(Icons.Default.BookmarkBorder, contentDescription = "Bookmark")
86
+ }
87
+ },
88
+ )
89
+ ```
@@ -293,70 +293,7 @@ NavHost(
293
293
  Integrate `NavigationBar` (Material 3) with NavHost. Each tab maintains its
294
294
  own back stack.
295
295
 
296
- ```kotlin
297
- @Serializable
298
- object HomeRoute
299
-
300
- @Serializable
301
- object SearchRoute
302
-
303
- @Serializable
304
- object ProfileRoute
305
-
306
- enum class TopLevelDestination(
307
- val label: String,
308
- val icon: ImageVector,
309
- val route: Any,
310
- ) {
311
- HOME("Home", Icons.Default.Home, HomeRoute),
312
- SEARCH("Search", Icons.Default.Search, SearchRoute),
313
- PROFILE("Profile", Icons.Default.Person, ProfileRoute),
314
- }
315
-
316
- @Composable
317
- fun MainScreen() {
318
- val navController = rememberNavController()
319
- val currentBackStackEntry by navController.currentBackStackEntryAsState()
320
-
321
- Scaffold(
322
- bottomBar = {
323
- NavigationBar {
324
- TopLevelDestination.entries.forEach { destination ->
325
- val isSelected = currentBackStackEntry
326
- ?.destination?.hasRoute(destination.route::class) == true
327
-
328
- NavigationBarItem(
329
- selected = isSelected,
330
- onClick = {
331
- navController.navigate(destination.route) {
332
- popUpTo(navController.graph.findStartDestination().id) {
333
- saveState = true
334
- }
335
- launchSingleTop = true
336
- restoreState = true
337
- }
338
- },
339
- icon = {
340
- Icon(destination.icon, contentDescription = destination.label)
341
- },
342
- label = { Text(destination.label) },
343
- )
344
- }
345
- }
346
- },
347
- ) { innerPadding ->
348
- NavHost(
349
- navController = navController,
350
- startDestination = HomeRoute,
351
- modifier = Modifier.padding(innerPadding),
352
- ) {
353
- composable<HomeRoute> { HomeScreen() }
354
- composable<SearchRoute> { SearchScreen() }
355
- composable<ProfileRoute> { ProfileScreen() }
356
- }
357
- }
358
- }
359
- ```
296
+ Code: [Bottom Navigation](references/code-examples.md#bottom-navigation).
360
297
 
361
298
  Key points for bottom nav:
362
299
  - Use `saveState = true` and `restoreState = true` to preserve tab back stacks
@@ -498,3 +435,7 @@ fun EditScreen(
498
435
  - [ ] Predictive back is enabled in manifest for Android 14+
499
436
  - [ ] Nested graphs use `navigation<>()` with explicit start destination
500
437
  - [ ] No navigation calls inside composable body without guard
438
+
439
+ ## References
440
+
441
+ - [Code examples](references/code-examples.md): open for the full code behind Bottom Navigation.