@mmerterden/multi-agent-pipeline 20.0.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 (90) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +5 -5
  3. package/README.tr.md +5 -5
  4. package/SECURITY.md +3 -3
  5. package/docs/adr/0011-dormant-ci.md +10 -1
  6. package/docs/architecture.md +2 -2
  7. package/docs/ecosystem.md +5 -5
  8. package/docs/facts.json +8 -7
  9. package/install/_codex-agents.mjs +1 -1
  10. package/manifest.json +92 -64
  11. package/package.json +1 -1
  12. package/pipeline/agents/code-reviewer.md +2 -2
  13. package/pipeline/agents/dev-critic.md +5 -5
  14. package/pipeline/agents/security-auditor.md +80 -72
  15. package/pipeline/commands/figma-to-swiftui.md +1 -1
  16. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  17. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  18. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -1
  19. package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
  20. package/pipeline/commands/multi-agent/scan/SKILL.md +2 -2
  21. package/pipeline/commands/multi-agent/security-review/SKILL.md +52 -0
  22. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  23. package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
  24. package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -6
  25. package/pipeline/multi-agent-refs/features/security-audit.md +55 -0
  26. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  27. package/pipeline/multi-agent-refs/phases/phase-3-review.md +9 -15
  28. package/pipeline/multi-agent-refs/phases/phase-5-report.md +1 -1
  29. package/pipeline/multi-agent-refs/threat-model.md +39 -0
  30. package/pipeline/schemas/agent-state.schema.json +23 -0
  31. package/pipeline/schemas/phases.json +1 -2
  32. package/pipeline/schemas/prefs.schema.json +0 -4
  33. package/pipeline/schemas/reviewer-output.schema.json +99 -2
  34. package/pipeline/schemas/security-finding.schema.json +144 -0
  35. package/pipeline/scripts/_stack-routing.mjs +1 -0
  36. package/pipeline/scripts/gc-abandoned.sh +16 -9
  37. package/pipeline/scripts/render-work-summary.sh +7 -4
  38. package/pipeline/skills/.skill-manifest.json +47 -23
  39. package/pipeline/skills/.skills-index.json +75 -9
  40. package/pipeline/skills/shared/README.md +13 -7
  41. package/pipeline/skills/shared/core/multi-agent/SKILL.md +3 -4
  42. package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +2 -2
  43. package/pipeline/skills/shared/core/multi-agent-security-review/SKILL.md +29 -0
  44. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  45. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  46. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  47. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  48. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  49. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  50. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  51. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  52. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  53. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  54. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  55. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  56. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  57. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  58. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  59. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  60. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  61. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  62. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  63. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  64. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  65. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  66. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  67. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  68. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  69. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  70. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  71. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  72. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  73. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  74. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  75. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  76. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  77. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  78. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  79. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  80. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  81. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  82. package/pipeline/skills/shared/external/security-review/SKILL.md +64 -0
  83. package/pipeline/skills/shared/external/security-review/references/owasp-mobile-top10-2024.md +53 -0
  84. package/pipeline/skills/shared/external/security-review/references/owasp-web-api-top10-2021.md +56 -0
  85. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  86. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  87. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  88. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  89. package/pipeline/skills/skills-index.md +8 -2
  90. package/pipeline/commands/security-review.md +0 -6
@@ -0,0 +1,257 @@
1
+ # Android MVI BaseViewModel Patterns
2
+
3
+ Full code examples backing the guidance in `SKILL.md`. Load this file when you
4
+ need a complete implementation to adapt. Headings mirror the SKILL.md sections.
5
+
6
+ ## Contents
7
+
8
+ - [Assembled BaseViewModel](#assembled-baseviewmodel)
9
+ - [MVI contracts for a screen](#mvi-contracts-for-a-screen)
10
+ - [A concrete ViewModel](#a-concrete-viewmodel)
11
+ - [Lifecycle-aware event collector](#lifecycle-aware-event-collector)
12
+ - [Stateless screen composable](#stateless-screen-composable)
13
+ - [Testing the ViewModel](#testing-the-viewmodel)
14
+
15
+ ## Assembled BaseViewModel
16
+
17
+ The complete base class combining state, events, intent dispatch, the use-case
18
+ executor, reference-counted loading, and the job registry.
19
+
20
+ ```kotlin
21
+ import androidx.lifecycle.ViewModel
22
+ import androidx.lifecycle.viewModelScope
23
+ import kotlinx.coroutines.CancellationException
24
+ import kotlinx.coroutines.Job
25
+ import kotlinx.coroutines.channels.Channel
26
+ import kotlinx.coroutines.flow.Flow
27
+ import kotlinx.coroutines.flow.MutableStateFlow
28
+ import kotlinx.coroutines.flow.SharingStarted
29
+ import kotlinx.coroutines.flow.StateFlow
30
+ import kotlinx.coroutines.flow.asStateFlow
31
+ import kotlinx.coroutines.flow.map
32
+ import kotlinx.coroutines.flow.receiveAsFlow
33
+ import kotlinx.coroutines.flow.stateIn
34
+ import kotlinx.coroutines.flow.update
35
+ import kotlinx.coroutines.launch
36
+
37
+ abstract class BaseViewModel<State, Intent, Event>(
38
+ initialState: State,
39
+ ) : ViewModel() {
40
+
41
+ private val _state = MutableStateFlow(initialState)
42
+ val state: StateFlow<State> = _state.asStateFlow()
43
+
44
+ private val _events = Channel<Event>(Channel.RENDEZVOUS)
45
+ val events: Flow<Event> = _events.receiveAsFlow()
46
+
47
+ private val loadingCount = MutableStateFlow(0)
48
+ val isLoading: StateFlow<Boolean> =
49
+ loadingCount
50
+ .map { it > 0 }
51
+ .stateIn(viewModelScope, SharingStarted.Eagerly, false)
52
+
53
+ private val jobs = mutableMapOf<String, Job>()
54
+
55
+ fun processIntent(intent: Intent) = handleIntent(intent)
56
+
57
+ protected abstract fun handleIntent(intent: Intent)
58
+
59
+ protected open fun routeError(throwable: Throwable) = Unit
60
+
61
+ protected fun reduce(reducer: State.() -> State) {
62
+ _state.update { it.reducer() }
63
+ }
64
+
65
+ protected fun sendEvent(event: Event) {
66
+ viewModelScope.launch { _events.send(event) }
67
+ }
68
+
69
+ protected fun <T> execute(
70
+ showLoading: Boolean = true,
71
+ onError: ((Throwable) -> Unit)? = null,
72
+ block: suspend () -> T,
73
+ onSuccess: (T) -> Unit,
74
+ ): Job = viewModelScope.launch {
75
+ if (showLoading) incrementLoading()
76
+ try {
77
+ runCatching { block() }
78
+ .onSuccess(onSuccess)
79
+ .onFailure { throwable ->
80
+ if (throwable is CancellationException) throw throwable
81
+ (onError ?: ::routeError)(throwable)
82
+ }
83
+ } finally {
84
+ if (showLoading) decrementLoading()
85
+ }
86
+ }
87
+
88
+ protected fun launchTracked(key: String, block: suspend () -> Unit): Job {
89
+ jobs.remove(key)?.cancel()
90
+ return viewModelScope.launch { block() }.also { jobs[key] = it }
91
+ }
92
+
93
+ protected fun cancel(key: String) {
94
+ jobs.remove(key)?.cancel()
95
+ }
96
+
97
+ private fun incrementLoading() = loadingCount.update { it + 1 }
98
+
99
+ private fun decrementLoading() =
100
+ loadingCount.update { (it - 1).coerceAtLeast(0) }
101
+
102
+ override fun onCleared() {
103
+ jobs.values.forEach { it.cancel() }
104
+ jobs.clear()
105
+ super.onCleared()
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## MVI contracts for a screen
111
+
112
+ State is the render model, Intent is every input, Event is every one-shot
113
+ effect. Keep all three next to the screen they serve.
114
+
115
+ ```kotlin
116
+ @Immutable
117
+ data class SearchState(
118
+ val query: String = "",
119
+ val results: List<ResultItem> = emptyList(),
120
+ val error: String? = null,
121
+ )
122
+
123
+ sealed interface SearchIntent {
124
+ data class QueryChanged(val text: String) : SearchIntent
125
+ data object Submit : SearchIntent
126
+ data object Retry : SearchIntent
127
+ }
128
+
129
+ sealed interface SearchEvent {
130
+ data class OpenDetail(val id: Id) : SearchEvent
131
+ data class ShowError(val message: String) : SearchEvent
132
+ }
133
+ ```
134
+
135
+ ## A concrete ViewModel
136
+
137
+ The subclass supplies the initial state, implements the exhaustive `when`, and
138
+ uses the inherited helpers - no coroutine, loading, or error boilerplate.
139
+
140
+ ```kotlin
141
+ @HiltViewModel
142
+ class SearchViewModel @Inject constructor(
143
+ private val search: SearchUseCase,
144
+ ) : BaseViewModel<SearchState, SearchIntent, SearchEvent>(SearchState()) {
145
+
146
+ override fun handleIntent(intent: SearchIntent) = when (intent) {
147
+ is SearchIntent.QueryChanged -> reduce { copy(query = intent.text) }
148
+ SearchIntent.Submit -> runSearch()
149
+ SearchIntent.Retry -> runSearch()
150
+ }
151
+
152
+ private fun runSearch() {
153
+ val query = state.value.query
154
+ launchTracked("search") {
155
+ execute(
156
+ block = { search(query) },
157
+ onError = { sendEvent(SearchEvent.ShowError(it.toUserMessage())) },
158
+ onSuccess = { results -> reduce { copy(results = results, error = null) } },
159
+ )
160
+ }
161
+ }
162
+
163
+ override fun routeError(throwable: Throwable) {
164
+ sendEvent(SearchEvent.ShowError(throwable.toUserMessage()))
165
+ }
166
+ }
167
+ ```
168
+
169
+ Keying `launchTracked("search")` makes each new submit cancel the previous
170
+ in-flight request automatically.
171
+
172
+ ## Lifecycle-aware event collector
173
+
174
+ Collect one-shot events only while the screen is at least `STARTED`, so a
175
+ backgrounded screen never consumes a navigation. `flowWithLifecycle` restarts
176
+ collection across stop/start without leaking the previous coroutine.
177
+
178
+ ```kotlin
179
+ @Composable
180
+ fun <Event> ObserveEvents(
181
+ events: Flow<Event>,
182
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
183
+ onEvent: (Event) -> Unit,
184
+ ) {
185
+ LaunchedEffect(events, lifecycleOwner) {
186
+ events
187
+ .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
188
+ .collect(onEvent)
189
+ }
190
+ }
191
+ ```
192
+
193
+ ## Stateless screen composable
194
+
195
+ The screen receives a state value and lambdas only. It has no ViewModel
196
+ reference, so it renders in a `@Preview` and in tests without Hilt.
197
+
198
+ ```kotlin
199
+ @Composable
200
+ fun SearchScreen(
201
+ state: SearchState,
202
+ isLoading: Boolean,
203
+ onIntent: (SearchIntent) -> Unit,
204
+ ) {
205
+ Column {
206
+ SearchField(
207
+ value = state.query,
208
+ onValueChange = { onIntent(SearchIntent.QueryChanged(it)) },
209
+ onSubmit = { onIntent(SearchIntent.Submit) },
210
+ )
211
+ when {
212
+ isLoading -> LoadingIndicator()
213
+ state.error != null -> ErrorState(
214
+ message = state.error,
215
+ onRetry = { onIntent(SearchIntent.Retry) },
216
+ )
217
+ else -> ResultList(items = state.results)
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ ## Testing the ViewModel
224
+
225
+ Drive intents, assert on state with Turbine, and assert one-shot events on the
226
+ `events` flow. Use a `TestDispatcher` so `viewModelScope` is controllable.
227
+
228
+ ```kotlin
229
+ @Test
230
+ fun submit_emitsResults() = runTest {
231
+ val viewModel = SearchViewModel(FakeSearchUseCase(results = sample))
232
+
233
+ viewModel.state.test {
234
+ assertEquals(SearchState(), awaitItem())
235
+
236
+ viewModel.processIntent(SearchIntent.QueryChanged("abc"))
237
+ assertEquals("abc", awaitItem().query)
238
+
239
+ viewModel.processIntent(SearchIntent.Submit)
240
+ assertEquals(sample, awaitItem().results)
241
+ }
242
+ }
243
+
244
+ @Test
245
+ fun useCaseFailure_emitsShowErrorEvent() = runTest {
246
+ val viewModel = SearchViewModel(FakeSearchUseCase(error = IOException()))
247
+
248
+ viewModel.events.test {
249
+ viewModel.processIntent(SearchIntent.Submit)
250
+ assertIs<SearchEvent.ShowError>(awaitItem())
251
+ }
252
+ }
253
+ ```
254
+
255
+ Set the main dispatcher to a `StandardTestDispatcher` in `@Before` /
256
+ `Dispatchers.setMain(...)` and reset it in `@After` so `viewModelScope`
257
+ coroutines run under test control.