@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/docs/facts.json +5 -5
  3. package/manifest.json +52 -31
  4. package/package.json +1 -1
  5. package/pipeline/skills/.skill-manifest.json +36 -20
  6. package/pipeline/skills/.skills-index.json +75 -9
  7. package/pipeline/skills/shared/README.md +13 -7
  8. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  9. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  10. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  11. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  12. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  13. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  14. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  15. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  16. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  17. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  18. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  19. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  20. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  21. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  22. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  23. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  24. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  25. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  26. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  27. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  28. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  29. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  30. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  31. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  32. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  33. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  34. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  35. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  36. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  37. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  38. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  39. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  40. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  41. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  42. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  43. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  44. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  45. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  46. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  47. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  48. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  49. package/pipeline/skills/skills-index.md +8 -2
@@ -70,6 +70,78 @@ val searchResults: Flow<List<Item>> = searchQuery
70
70
  val uiState: StateFlow<UiState> = _uiState.asStateFlow()
71
71
  ```
72
72
 
73
+ ### 4. Injectable Dispatchers
74
+
75
+ Never hardcode `Dispatchers.IO` / `Dispatchers.Default` inside repositories and data sources. Inject them through Hilt qualifiers so a test can swap in a `TestDispatcher` and control virtual time without touching production code.
76
+
77
+ ```kotlin
78
+ @Retention(AnnotationRetention.BINARY)
79
+ @Qualifier
80
+ annotation class IoDispatcher
81
+
82
+ @Retention(AnnotationRetention.BINARY)
83
+ @Qualifier
84
+ annotation class DefaultDispatcher
85
+
86
+ @Module
87
+ @InstallIn(SingletonComponent::class)
88
+ object DispatchersModule {
89
+ @IoDispatcher
90
+ @Provides
91
+ fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
92
+
93
+ @DefaultDispatcher
94
+ @Provides
95
+ fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
96
+ }
97
+
98
+ class ItemRepository @Inject constructor(
99
+ private val api: ItemApi,
100
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher
101
+ ) {
102
+ suspend fun load(id: String): Item = withContext(ioDispatcher) {
103
+ api.fetch(id).toDomain()
104
+ }
105
+ }
106
+ ```
107
+
108
+ In tests, provide a single `TestDispatcher` (typically `StandardTestDispatcher`) for both qualifiers so `runTest`'s scheduler drives everything.
109
+
110
+ The `DispatcherProvider` interface idiom (one injected object exposing `io`, `default`, `main`) is a valid alternative and helps when a class needs several dispatchers, but the per-dispatcher qualifier form is leaner: it names the exact dependency at the constructor and avoids a wrapper type.
111
+
112
+ ### 5. Cancellation-Safe Error Handling
113
+
114
+ A broad `catch` swallows `CancellationException` and silently breaks structured concurrency: the coroutine keeps running after its scope was cancelled. Rethrow it explicitly before any `catch (Throwable)` or `catch (Exception)`.
115
+
116
+ ```kotlin
117
+ suspend fun loadOrFallback(): Data =
118
+ try {
119
+ repo.fetch()
120
+ } catch (e: CancellationException) {
121
+ throw e
122
+ } catch (e: Throwable) {
123
+ Data.empty()
124
+ }
125
+ ```
126
+
127
+ Choose the strategy per branch. On the critical branch, let cancellation and real failures propagate so the caller sees them. On an optional parallel branch, degrade to a fallback after the rethrow.
128
+
129
+ ```kotlin
130
+ suspend fun loadScreen(): ScreenData = coroutineScope {
131
+ val core = async { repo.loadCore() }
132
+ val banner = async {
133
+ try {
134
+ repo.loadPromoBanner()
135
+ } catch (e: CancellationException) {
136
+ throw e
137
+ } catch (e: Throwable) {
138
+ null
139
+ }
140
+ }
141
+ ScreenData(core = core.await(), banner = banner.await())
142
+ }
143
+ ```
144
+
73
145
  ## Examples
74
146
 
75
147
  ### Example 1: Parallel Execution with Error Handling
@@ -87,6 +159,77 @@ suspend fun fetchDataWithErrorHandling() = supervisorScope {
87
159
  }
88
160
  ```
89
161
 
162
+ ### Example 2: Fire-and-Forget Background Persistence
163
+
164
+ A long-lived scope built on `SupervisorJob` for writes whose failure must not cancel siblings or crash the caller. The body is always guarded, because an uncaught throw in a supervisor child still reaches the scope's exception handler.
165
+
166
+ ```kotlin
167
+ class CacheWriter @Inject constructor(
168
+ @IoDispatcher ioDispatcher: CoroutineDispatcher
169
+ ) {
170
+ private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
171
+
172
+ fun persist(entry: CacheEntry) {
173
+ scope.launch {
174
+ try {
175
+ dao.upsert(entry)
176
+ } catch (e: CancellationException) {
177
+ throw e
178
+ } catch (e: Throwable) {
179
+ logger.warn("cache write failed", e)
180
+ }
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### Example 3: Double-Checked Lazy Async Init in a Suspend Context
187
+
188
+ Guard a one-time async initialization with `Mutex.withLock`, not `synchronized`. `synchronized` blocks the thread and cannot host suspend calls; `withLock` suspends the coroutine and releases cooperatively.
189
+
190
+ ```kotlin
191
+ class TokenProvider @Inject constructor(private val api: AuthApi) {
192
+ private val mutex = Mutex()
193
+ @Volatile private var cached: Token? = null
194
+
195
+ suspend fun token(): Token {
196
+ cached?.let { return it }
197
+ return mutex.withLock {
198
+ cached ?: api.fetchToken().also { cached = it }
199
+ }
200
+ }
201
+ }
202
+ ```
203
+
204
+ ### Example 4: Awaiting a Suspend Result Inside a Synchronous Callback
205
+
206
+ A framework callback that must return a value synchronously cannot call a suspend function directly, and `runBlocking` on the main or a callback thread risks deadlock. Bridge with a scoped `launch`, a lock/condition, and `withTimeout` so a stalled producer cannot block forever.
207
+
208
+ ```kotlin
209
+ fun interceptSync(request: Request): Response {
210
+ val lock = ReentrantLock()
211
+ val done = lock.newCondition()
212
+ var result: Response? = null
213
+
214
+ scope.launch {
215
+ val value = try {
216
+ withTimeout(5_000) { resolver.resolve(request) }
217
+ } catch (e: Throwable) {
218
+ Response.passthrough(request)
219
+ }
220
+ lock.withLock {
221
+ result = value
222
+ done.signalAll()
223
+ }
224
+ }
225
+
226
+ lock.withLock {
227
+ while (result == null) done.await()
228
+ return result!!
229
+ }
230
+ }
231
+ ```
232
+
90
233
  ## Best Practices
91
234
 
92
235
  - ✅ **Do:** Use `Dispatchers.IO` for blocking I/O operations.
@@ -130,29 +130,10 @@ Map {
130
130
  ### Camera Position
131
131
 
132
132
  `MapCameraPosition` controls what the map displays. Bind it to let the user
133
- interact and to programmatically move the camera.
134
-
135
- ```swift
136
- // Center on a region
137
- @State private var position: MapCameraPosition = .region(
138
- MKCoordinateRegion(
139
- center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
140
- span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
141
- )
142
- )
143
-
144
- // Follow user location
145
- @State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
146
-
147
- // Specific camera angle (3D perspective)
148
- @State private var position: MapCameraPosition = .camera(
149
- MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
150
- )
151
-
152
- // Frame specific items
153
- position = .item(MKMapItem.forCurrentLocation())
154
- position = .rect(MKMapRect(...))
155
- ```
133
+ interact and to programmatically move the camera. Initialize it with `.region`,
134
+ `.userLocation(fallback:)`, `.camera`, `.item`, `.rect`, or `.automatic`. See
135
+ [references/mapkit-patterns.md](references/mapkit-patterns.md) ("Camera Control") for the position
136
+ initializers, animated fly-to, framing content, and reading the visible region.
156
137
 
157
138
  ### Map Style
158
139
 
@@ -308,76 +289,39 @@ if let placemark = placemarks.first {
308
289
 
309
290
  ### MKGeocodingRequest and MKReverseGeocodingRequest (iOS 26+)
310
291
 
311
- New MapKit-native geocoding that returns `MKMapItem` with richer data and
312
- `MKAddress` / `MKAddressRepresentations` for flexible address formatting.
313
-
314
- ```swift
315
- @available(iOS 26, *)
316
- func reverseGeocode(location: CLLocation) async throws -> MKMapItem? {
317
- guard let request = MKReverseGeocodingRequest(location: location) else {
318
- return nil
319
- }
320
- let mapItems = try await request.mapItems
321
- return mapItems.first
322
- }
323
-
324
- @available(iOS 26, *)
325
- func forwardGeocode(address: String) async throws -> [MKMapItem] {
326
- guard let request = MKGeocodingRequest(addressString: address) else { return [] }
327
- return try await request.mapItems
328
- }
329
- ```
292
+ New MapKit-native geocoding returns `MKMapItem` with richer data and `MKAddress`
293
+ / `MKAddressRepresentations` for flexible address formatting. Construct
294
+ `MKGeocodingRequest(addressString:)` or `MKReverseGeocodingRequest(location:)` and
295
+ await `.mapItems`. See [references/mapkit-patterns.md](references/mapkit-patterns.md) ("iOS 26 New
296
+ APIs") for both, plus `MKAddressRepresentations` formatting.
330
297
 
331
298
  ## Search
332
299
 
333
300
  ### MKLocalSearchCompleter (Autocomplete)
334
301
 
335
- ```swift
336
- @Observable
337
- final class SearchCompleter: NSObject, MKLocalSearchCompleterDelegate {
338
- var results: [MKLocalSearchCompletion] = []
339
- var query: String = "" { didSet { completer.queryFragment = query } }
340
-
341
- private let completer = MKLocalSearchCompleter()
342
-
343
- override init() {
344
- super.init()
345
- completer.delegate = self
346
- completer.resultTypes = [.address, .pointOfInterest]
347
- }
348
-
349
- func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
350
- results = completer.results
351
- }
352
-
353
- func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
354
- results = []
355
- }
356
- }
357
- ```
302
+ Set `queryFragment` on an `MKLocalSearchCompleter` and read `results` from its
303
+ delegate for suggestions. Debounce input (300ms+) and constrain `completer.region`
304
+ to the visible map region.
358
305
 
359
306
  ### MKLocalSearch (Full Search)
360
307
 
308
+ Convert a selected completion (or a natural-language query) into full `MKMapItem`
309
+ results:
310
+
361
311
  ```swift
362
312
  func search(for completion: MKLocalSearchCompletion) async throws -> [MKMapItem] {
363
313
  let request = MKLocalSearch.Request(completion: completion)
364
314
  request.resultTypes = [.pointOfInterest, .address]
365
- let search = MKLocalSearch(request: request)
366
- let response = try await search.start()
367
- return response.mapItems
368
- }
369
-
370
- // Search by natural language query within a region
371
- func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
372
- let request = MKLocalSearch.Request()
373
- request.naturalLanguageQuery = query
374
- request.region = region
375
- let search = MKLocalSearch(request: request)
376
- let response = try await search.start()
315
+ let response = try await MKLocalSearch(request: request).start()
377
316
  return response.mapItems
378
317
  }
379
318
  ```
380
319
 
320
+ For a natural-language query, set `request.naturalLanguageQuery` and
321
+ `request.region` instead. See [references/mapkit-patterns.md](references/mapkit-patterns.md) ("Search
322
+ with Autocomplete") for the full completer delegate class and a `.searchable`
323
+ view integration.
324
+
381
325
  ## Directions
382
326
 
383
327
  ```swift
@@ -411,32 +355,13 @@ Map {
411
355
  }
412
356
  ```
413
357
 
414
- ### ETA Calculation
358
+ ### ETA and Cycling Directions
415
359
 
416
- ```swift
417
- func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
418
- let request = MKDirections.Request()
419
- request.source = source
420
- request.destination = destination
421
- let directions = MKDirections(request: request)
422
- let response = try await directions.calculateETA()
423
- return response.expectedTravelTime
424
- }
425
- ```
426
-
427
- ### Cycling Directions (iOS 14+)
428
-
429
- ```swift
430
- func getCyclingDirections(to destination: MKMapItem) async throws -> MKRoute? {
431
- let request = MKDirections.Request()
432
- request.source = MKMapItem.forCurrentLocation()
433
- request.destination = destination
434
- request.transportType = .cycling
435
- let directions = MKDirections(request: request)
436
- let response = try await directions.calculate()
437
- return response.routes.first
438
- }
439
- ```
360
+ For travel time only, use `MKDirections.calculateETA()` (returns
361
+ `expectedTravelTime` without route geometry). For bike routes, set
362
+ `request.transportType = .cycling` (iOS 14+). See
363
+ [references/mapkit-patterns.md](references/mapkit-patterns.md) ("ETA Calculation" and "Cycling
364
+ Directions") for both.
440
365
 
441
366
  ## PlaceDescriptor (iOS 26+)
442
367
 
@@ -137,6 +137,33 @@ Annotation(place.name, coordinate: place.coordinate, anchor: .bottom) {
137
137
 
138
138
  ## Camera Control (MapCameraPosition)
139
139
 
140
+ ### Setting an initial position
141
+
142
+ `MapCameraPosition` controls what the map displays. Bind it to let the user
143
+ interact and to move the camera programmatically.
144
+
145
+ ```swift
146
+ // Center on a region
147
+ @State private var position: MapCameraPosition = .region(
148
+ MKCoordinateRegion(
149
+ center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
150
+ span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
151
+ )
152
+ )
153
+
154
+ // Follow user location
155
+ @State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
156
+
157
+ // Specific camera angle (3D perspective)
158
+ @State private var position: MapCameraPosition = .camera(
159
+ MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
160
+ )
161
+
162
+ // Frame specific items
163
+ position = .item(MKMapItem.forCurrentLocation())
164
+ position = .rect(MKMapRect(...))
165
+ ```
166
+
140
167
  ### Animate camera changes
141
168
 
142
169
  Wrap position updates in `withAnimation` for smooth transitions:
@@ -389,6 +416,21 @@ ForEach(Array(response.routes.enumerated()), id: \.offset) { index, route in
389
416
  }
390
417
  ```
391
418
 
419
+ ### ETA Calculation
420
+
421
+ Use `calculateETA()` when you only need travel time, not the full route geometry.
422
+
423
+ ```swift
424
+ func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
425
+ let request = MKDirections.Request()
426
+ request.source = source
427
+ request.destination = destination
428
+ let directions = MKDirections(request: request)
429
+ let response = try await directions.calculateETA()
430
+ return response.expectedTravelTime
431
+ }
432
+ ```
433
+
392
434
  ---
393
435
 
394
436
  ## Look Around Preview