@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
@@ -0,0 +1,200 @@
1
+ # Jetpack Compose Components Patterns
2
+
3
+ Full code examples backing the guidance in `SKILL.md`. Headings mirror the
4
+ SKILL.md sections.
5
+
6
+ ## Contents
7
+
8
+ - [Two-Layer Design Tokens](#two-layer-design-tokens)
9
+ - [Bridging Tokens to Material 3](#bridging-tokens-to-material-3)
10
+ - [Component Catalog](#component-catalog)
11
+
12
+ ## Two-Layer Design Tokens
13
+
14
+ Layer 1 is a machine-generated raw-token source: one file, never hand-edited,
15
+ the single source of truth for every primitive value. Layer 2 is a hand-authored
16
+ semantic wrapper: `@Immutable` data classes that name each role and reference the
17
+ raw tokens, never a literal hex or `sp`. Each semantic model exposes a `light()`
18
+ and a `dark()` variant.
19
+
20
+ ```kotlin
21
+ // Layer 1: generated, do not edit. Produced by the token codegen pipeline.
22
+ object RawTokens {
23
+ val neutral0 = Color(0xFFFFFFFF)
24
+ val neutral900 = Color(0xFF1C1B1F)
25
+ val neutral1000 = Color(0xFF000000)
26
+ val red600 = Color(0xFFBA1A1A)
27
+ val red200 = Color(0xFFFFB4AB)
28
+ val fontSizeBody = 16.sp
29
+ val lineHeightBody = 24.sp
30
+ val fontSizeTitle = 22.sp
31
+ val lineHeightTitle = 28.sp
32
+ }
33
+ ```
34
+
35
+ ```kotlin
36
+ // Layer 2: hand-authored semantic wrapper.
37
+ @Immutable
38
+ data class AppColors(
39
+ val background: Color,
40
+ val onBackground: Color,
41
+ val brandPrimary: Color,
42
+ val brandOnPrimary: Color,
43
+ val surface: Color,
44
+ val danger: Color,
45
+ ) {
46
+ companion object {
47
+ fun light() = AppColors(
48
+ background = RawTokens.neutral0,
49
+ onBackground = RawTokens.neutral900,
50
+ brandPrimary = RawTokens.red600,
51
+ brandOnPrimary = RawTokens.neutral0,
52
+ surface = RawTokens.neutral0,
53
+ danger = RawTokens.red600,
54
+ )
55
+
56
+ fun dark() = AppColors(
57
+ background = RawTokens.neutral1000,
58
+ onBackground = RawTokens.neutral0,
59
+ brandPrimary = RawTokens.red200,
60
+ brandOnPrimary = RawTokens.neutral900,
61
+ surface = RawTokens.neutral1000,
62
+ danger = RawTokens.red200,
63
+ )
64
+ }
65
+ }
66
+
67
+ @Immutable
68
+ data class AppTypography(
69
+ val body: TextStyle,
70
+ val title: TextStyle,
71
+ ) {
72
+ companion object {
73
+ fun default() = AppTypography(
74
+ body = TextStyle(
75
+ fontSize = RawTokens.fontSizeBody,
76
+ lineHeight = RawTokens.lineHeightBody,
77
+ ),
78
+ title = TextStyle(
79
+ fontSize = RawTokens.fontSizeTitle,
80
+ lineHeight = RawTokens.lineHeightTitle,
81
+ fontWeight = FontWeight.Medium,
82
+ ),
83
+ )
84
+ }
85
+ }
86
+ ```
87
+
88
+ The codegen pipeline that produces Layer 1 is a separate concern; see the
89
+ `android-design-tokens-codegen` skill. This skill covers the consumption side
90
+ only.
91
+
92
+ ## Bridging Tokens to Material 3
93
+
94
+ Bridge the semantic tokens to both worlds. Select an M3
95
+ `light`/`darkColorScheme` built from the tokens so stock Material components
96
+ (`Button`, `TopAppBar`, `Card`) inherit the brand colors, and expose the richer
97
+ token set through `CompositionLocalProvider`. Read the rich set through a
98
+ `Theme.colors` / `Theme.typography` accessor marked `@ReadOnlyComposable`.
99
+
100
+ ```kotlin
101
+ val LocalAppColors = staticCompositionLocalOf { AppColors.light() }
102
+ val LocalAppTypography = staticCompositionLocalOf { AppTypography.default() }
103
+
104
+ private fun AppColors.toMaterialColorScheme(darkTheme: Boolean): ColorScheme {
105
+ val base = if (darkTheme) darkColorScheme() else lightColorScheme()
106
+ return base.copy(
107
+ primary = brandPrimary,
108
+ onPrimary = brandOnPrimary,
109
+ background = background,
110
+ onBackground = onBackground,
111
+ surface = surface,
112
+ error = danger,
113
+ )
114
+ }
115
+
116
+ private fun AppTypography.toMaterialTypography(): Typography =
117
+ Typography(bodyLarge = body, titleLarge = title)
118
+
119
+ @Composable
120
+ fun AppTheme(
121
+ darkTheme: Boolean = isSystemInDarkTheme(),
122
+ content: @Composable () -> Unit,
123
+ ) {
124
+ val colors = if (darkTheme) AppColors.dark() else AppColors.light()
125
+ val typography = AppTypography.default()
126
+
127
+ CompositionLocalProvider(
128
+ LocalAppColors provides colors,
129
+ LocalAppTypography provides typography,
130
+ ) {
131
+ MaterialTheme(
132
+ colorScheme = colors.toMaterialColorScheme(darkTheme),
133
+ typography = typography.toMaterialTypography(),
134
+ content = content,
135
+ )
136
+ }
137
+ }
138
+
139
+ object Theme {
140
+ val colors: AppColors
141
+ @Composable @ReadOnlyComposable get() = LocalAppColors.current
142
+ val typography: AppTypography
143
+ @Composable @ReadOnlyComposable get() = LocalAppTypography.current
144
+ }
145
+ ```
146
+
147
+ Stock Material components read `MaterialTheme.colorScheme`; custom components read
148
+ the richer set:
149
+
150
+ ```kotlin
151
+ @Composable
152
+ fun PriceTag(text: String) {
153
+ Text(
154
+ text = text,
155
+ color = Theme.colors.brandPrimary,
156
+ style = Theme.typography.title,
157
+ )
158
+ }
159
+ ```
160
+
161
+ ## Component Catalog
162
+
163
+ A component-catalog module (for example Showkase) renders every annotated
164
+ composable and color/typography token in a browsable in-app gallery. Keep the
165
+ annotation cheap and universal so it can sit on previews across every module, but
166
+ gate the KSP processor behind a build flag so the browser codegen costs nothing
167
+ on a normal build.
168
+
169
+ ```kotlin
170
+ // A preview, annotated for the catalog. The annotation dependency is universal.
171
+ @ShowkaseComposable(name = "InfoCard", group = "Cards")
172
+ @Preview
173
+ @Composable
174
+ private fun InfoCardCatalog() {
175
+ AppTheme { InfoCard(/* sample slots */) }
176
+ }
177
+ ```
178
+
179
+ ```kotlin
180
+ // ui module build.gradle.kts
181
+ dependencies {
182
+ implementation(libs.showkase.annotation) // cheap, always present
183
+ if (providers.gradleProperty("enableShowkase").isPresent) {
184
+ ksp(libs.showkase.processor) // browser codegen only when asked
185
+ }
186
+ }
187
+ ```
188
+
189
+ ```kotlin
190
+ // The catalog entry point, compiled only when the processor ran.
191
+ class CatalogActivity : ComponentActivity() {
192
+ override fun onCreate(savedInstanceState: Bundle?) {
193
+ super.onCreate(savedInstanceState)
194
+ setContent { ShowkaseBrowser(getMetadata()) }
195
+ }
196
+ }
197
+ ```
198
+
199
+ Build the catalog on demand with `./gradlew :app:installDebug -PenableShowkase`;
200
+ routine builds skip the processor entirely.
@@ -16,6 +16,9 @@ passing.
16
16
  - [NavHost Setup](#navhost-setup)
17
17
  - [Navigating Between Screens](#navigating-between-screens)
18
18
  - [Arguments (Type-Safe)](#arguments-type-safe)
19
+ - [Custom NavType for Complex Arguments](#custom-navtype-for-complex-arguments)
20
+ - [SafeNavController via CompositionLocal](#safenavcontroller-via-compositionlocal)
21
+ - [Navigation as a One-Shot Effect](#navigation-as-a-one-shot-effect)
19
22
  - [Nested Navigation Graphs](#nested-navigation-graphs)
20
23
  - [Bottom Navigation](#bottom-navigation)
21
24
  - [Deep Links](#deep-links)
@@ -163,10 +166,70 @@ composable<FlightDetailRoute> { backStackEntry ->
163
166
  | `Int`, `Long`, `Float`, `Boolean` | Native | Serialized directly |
164
167
  | `Enum` | Via `@Serializable` | Add `@Serializable` to enum class |
165
168
  | `List<String>` | Via serialization | Works with kotlinx.serialization |
166
- | Custom objects | Not recommended | Pass ID and fetch from ViewModel instead |
169
+ | Custom objects | Via custom `NavType` | Register in a `typeMap` (see below) |
167
170
 
168
- Rule: Pass only primitive identifiers via routes. Let the destination ViewModel
169
- fetch the full object. Do not serialize complex objects into the route.
171
+ Rule: Prefer primitive IDs and fetch the full object in the destination
172
+ ViewModel; when a small value object must ride in the route, use a custom
173
+ `NavType` (below) rather than flattening it into primitives.
174
+
175
+ ## Custom NavType for Complex Arguments
176
+
177
+ Passing a complex `@Serializable` type through a route needs a custom `NavType`
178
+ in a `typeMap` shared by `composable<>()` and every `toRoute` read (else only primitives resolve).
179
+
180
+ ```kotlin
181
+ val searchTypeMap = mapOf(typeOf<PassengerFilter>() to PassengerFilterNavType)
182
+
183
+ composable<SearchRoute>(typeMap = searchTypeMap) { entry ->
184
+ val route = entry.toRoute<SearchRoute>(typeMap = searchTypeMap)
185
+ SearchScreen(filter = route.filter)
186
+ }
187
+ ```
188
+
189
+ A shared holder owns the one `typeMap`; the ViewModel reads the arg back with
190
+ `savedStateHandle.toRoute<SearchRoute>(typeMap = searchTypeMap)`. Full `NavType`
191
+ + holder: [references/patterns.md#custom-navtype-type-map](references/patterns.md#custom-navtype-type-map).
192
+
193
+ ## SafeNavController via CompositionLocal
194
+
195
+ Never give feature code a raw `NavHostController`. Wrap it to expose only the
196
+ methods features need, each in `runCatching` to swallow rapid-tap double-navigation / illegal-state crashes, and provide it via a `CompositionLocal`.
197
+
198
+ ```kotlin
199
+ class SafeNavController(private val controller: NavHostController) {
200
+ fun navigate(route: Any) = runCatching { controller.navigate(route) }
201
+ fun navigateReplacing(route: Any, popUpToType: KClass<*>) = runCatching {
202
+ controller.navigate(route) { popUpTo(popUpToType) { inclusive = true }; launchSingleTop = true }
203
+ }
204
+ fun back() = runCatching { controller.popBackStack() }
205
+ }
206
+
207
+ val LocalSafeNavController = staticCompositionLocalOf<SafeNavController> { error("not provided") }
208
+ ```
209
+
210
+ Provide once at the NavHost; screens read `LocalSafeNavController.current`. Full
211
+ setup + helpers: [references/patterns.md#safenavcontroller](references/patterns.md#safenavcontroller).
212
+
213
+ ## Navigation as a One-Shot Effect
214
+
215
+ Keep navigation types out of the ViewModel: it emits `NavigateX` events (a
216
+ `sealed interface`) on a one-shot `Channel`; the owning composable translates
217
+ each into a call, keeping the ViewModel navigation-free and unit-testable.
218
+
219
+ ```kotlin
220
+ LaunchedEffect(Unit) { // in the composable that owns the NavController
221
+ viewModel.navEvents.collect { event ->
222
+ when (event) {
223
+ is HomeNavEvent.ToDetail -> nav.navigate(DetailRoute(event.id))
224
+ HomeNavEvent.ToSearch -> nav.navigate(SearchRoute())
225
+ }
226
+ }
227
+ }
228
+ ```
229
+
230
+ Full `sealed interface` + ViewModel channel wiring:
231
+ [references/patterns.md#navigation-as-a-one-shot-effect](references/patterns.md#navigation-as-a-one-shot-effect).
232
+ Complements the event seam in the `android-mvi-viewmodel` skill.
170
233
 
171
234
  ## Nested Navigation Graphs
172
235
 
@@ -0,0 +1,191 @@
1
+ # Compose Navigation -- Reference Patterns
2
+
3
+ Full code for the patterns summarized in `SKILL.md`. Load the section you need
4
+ when implementing that specific navigation feature.
5
+
6
+ ## Contents
7
+
8
+ - [Custom NavType Type-Map](#custom-navtype-type-map)
9
+ - [SafeNavController](#safenavcontroller)
10
+ - [Navigation as a One-Shot Effect](#navigation-as-a-one-shot-effect)
11
+
12
+ ## Custom NavType Type-Map
13
+
14
+ Type-safe routes serialize primitives out of the box. A complex `@Serializable`
15
+ argument needs a custom `NavType` plus a `typeMap` shared by the
16
+ `composable<>()` registration and every `toRoute` read. Without the type-map,
17
+ only primitive nav args resolve and the route fails to build.
18
+
19
+ ```kotlin
20
+ @Serializable
21
+ data class PassengerFilter(val cabin: String, val adults: Int, val children: Int)
22
+
23
+ @Serializable
24
+ data class SearchRoute(val filter: PassengerFilter)
25
+
26
+ val PassengerFilterNavType = object : NavType<PassengerFilter>(isNullableAllowed = false) {
27
+ override fun get(bundle: Bundle, key: String): PassengerFilter? =
28
+ bundle.getString(key)?.let { Json.decodeFromString(it) }
29
+
30
+ override fun parseValue(value: String): PassengerFilter =
31
+ Json.decodeFromString(Uri.decode(value))
32
+
33
+ override fun serializeAsValue(value: PassengerFilter): String =
34
+ Uri.encode(Json.encodeToString(value))
35
+
36
+ override fun put(bundle: Bundle, key: String, value: PassengerFilter) {
37
+ bundle.putString(key, Json.encodeToString(value))
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### Shared holder
43
+
44
+ One holder owns the map so the registration and every read agree:
45
+
46
+ ```kotlin
47
+ object NavTypeMaps {
48
+ val search: Map<KType, NavType<*>> =
49
+ mapOf(typeOf<PassengerFilter>() to PassengerFilterNavType)
50
+ }
51
+ ```
52
+
53
+ ### Registration and read
54
+
55
+ ```kotlin
56
+ composable<SearchRoute>(typeMap = NavTypeMaps.search) { entry ->
57
+ val route = entry.toRoute<SearchRoute>(typeMap = NavTypeMaps.search)
58
+ SearchScreen(filter = route.filter)
59
+ }
60
+ ```
61
+
62
+ ### Reading in the ViewModel
63
+
64
+ `SavedStateHandle.toRoute` takes the same map:
65
+
66
+ ```kotlin
67
+ @HiltViewModel
68
+ class SearchViewModel @Inject constructor(
69
+ savedStateHandle: SavedStateHandle,
70
+ ) : ViewModel() {
71
+ private val filter: PassengerFilter =
72
+ savedStateHandle.toRoute<SearchRoute>(typeMap = NavTypeMaps.search).filter
73
+ }
74
+ ```
75
+
76
+ ## SafeNavController
77
+
78
+ Feature code should never hold a raw `NavHostController`. Wrap it so features see
79
+ only the methods they need, and wrap each call in `runCatching` so a
80
+ double-navigation or illegal-state exception from a rapid double tap is
81
+ swallowed instead of crashing.
82
+
83
+ ```kotlin
84
+ class SafeNavController(private val controller: NavHostController) {
85
+
86
+ fun navigate(route: Any) {
87
+ runCatching { controller.navigate(route) }
88
+ }
89
+
90
+ fun navigateReplacing(route: Any, popUpToType: KClass<*>) {
91
+ runCatching {
92
+ controller.navigate(route) {
93
+ popUpTo(popUpToType) { inclusive = true }
94
+ launchSingleTop = true
95
+ }
96
+ }
97
+ }
98
+
99
+ fun navigateClearingStack(route: Any) {
100
+ runCatching {
101
+ controller.navigate(route) { popUpTo(0) { inclusive = true } }
102
+ }
103
+ }
104
+
105
+ fun back() {
106
+ runCatching { controller.popBackStack() }
107
+ }
108
+ }
109
+
110
+ val LocalSafeNavController = staticCompositionLocalOf<SafeNavController> {
111
+ error("SafeNavController not provided")
112
+ }
113
+ ```
114
+
115
+ ### Providing it once at the NavHost
116
+
117
+ ```kotlin
118
+ @Composable
119
+ fun AppNavHost(navController: NavHostController = rememberNavController()) {
120
+ val safeNav = remember(navController) { SafeNavController(navController) }
121
+ CompositionLocalProvider(LocalSafeNavController provides safeNav) {
122
+ NavHost(navController = navController, startDestination = HomeRoute) {
123
+ composable<HomeRoute> { HomeRoute() }
124
+ composable<DetailRoute> { DetailRoute() }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ Feature screens read `LocalSafeNavController.current` and never receive the raw
131
+ controller as a parameter.
132
+
133
+ ## Navigation as a One-Shot Effect
134
+
135
+ The ViewModel emits navigation intents as one-shot events; the composable that
136
+ owns the `NavController` translates each into a navigation call. This keeps the
137
+ ViewModel free of navigation types and unit-testable (assert emitted events, no
138
+ `NavController` mock needed).
139
+
140
+ ### ViewModel side
141
+
142
+ ```kotlin
143
+ sealed interface HomeNavEvent {
144
+ data class ToDetail(val id: String) : HomeNavEvent
145
+ data object ToSearch : HomeNavEvent
146
+ }
147
+
148
+ @HiltViewModel
149
+ class HomeViewModel @Inject constructor() : ViewModel() {
150
+
151
+ private val _navEvents = Channel<HomeNavEvent>(Channel.BUFFERED)
152
+ val navEvents: Flow<HomeNavEvent> = _navEvents.receiveAsFlow()
153
+
154
+ fun onItemClick(id: String) {
155
+ viewModelScope.launch { _navEvents.send(HomeNavEvent.ToDetail(id)) }
156
+ }
157
+
158
+ fun onSearchClick() {
159
+ viewModelScope.launch { _navEvents.send(HomeNavEvent.ToSearch) }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Composable side
165
+
166
+ ```kotlin
167
+ @Composable
168
+ fun HomeRoute(viewModel: HomeViewModel = hiltViewModel()) {
169
+ val nav = LocalSafeNavController.current
170
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
171
+
172
+ LaunchedEffect(Unit) {
173
+ viewModel.navEvents.collect { event ->
174
+ when (event) {
175
+ is HomeNavEvent.ToDetail -> nav.navigate(DetailRoute(event.id))
176
+ HomeNavEvent.ToSearch -> nav.navigate(SearchRoute(PassengerFilter("ECONOMY", 1, 0)))
177
+ }
178
+ }
179
+ }
180
+
181
+ HomeScreen(
182
+ state = uiState,
183
+ onItemClick = viewModel::onItemClick,
184
+ onSearchClick = viewModel::onSearchClick,
185
+ )
186
+ }
187
+ ```
188
+
189
+ The `Channel` (not a `StateFlow`) guarantees each navigation fires once and is
190
+ not replayed on recomposition or config change. This complements the event seam
191
+ in the `android-mvi-viewmodel` skill.