@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.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 (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/facts.json +6 -6
  3. package/manifest.json +55 -34
  4. package/package.json +1 -1
  5. package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
  6. package/pipeline/schemas/prefs.schema.json +4 -0
  7. package/pipeline/scripts/usage-register.mjs +2 -0
  8. package/pipeline/skills/.skill-manifest.json +36 -20
  9. package/pipeline/skills/.skills-index.json +75 -9
  10. package/pipeline/skills/shared/README.md +13 -7
  11. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  12. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  13. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  14. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  15. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  16. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  17. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  18. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  19. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  20. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  21. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  22. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  23. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  24. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  25. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  26. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  27. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  28. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  29. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  30. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  31. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  32. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  33. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  34. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  35. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  36. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  37. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  38. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  39. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  40. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  41. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  42. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  43. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  44. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  45. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  46. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  47. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  48. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  49. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  50. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  51. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  52. package/pipeline/skills/skills-index.md +8 -2
@@ -0,0 +1,297 @@
1
+ # Android DataStore Patterns
2
+
3
+ Full code examples backing the guidance in `SKILL.md`. Headings mirror the
4
+ SKILL.md sections. Package, class, and key names are placeholders; adapt them.
5
+
6
+ ## Contents
7
+
8
+ - [Preferences DataStore](#preferences-datastore)
9
+ - [Typed DataStore with a Serializer](#typed-datastore-with-a-serializer)
10
+ - [Encrypted Typed DataStore](#encrypted-typed-datastore)
11
+ - [Migrating from SharedPreferences](#migrating-from-sharedpreferences)
12
+ - [Versioned Data Migrations](#versioned-data-migrations)
13
+ - [Reading and Writing as Flow](#reading-and-writing-as-flow)
14
+ - [Testing](#testing)
15
+
16
+ ## Preferences DataStore
17
+
18
+ ```kotlin
19
+ val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
20
+ name = "settings"
21
+ )
22
+
23
+ object SettingsKeys {
24
+ val THEME = stringPreferencesKey("theme")
25
+ val LAUNCH_COUNT = intPreferencesKey("launch_count")
26
+ val ONBOARDED = booleanPreferencesKey("onboarded")
27
+ }
28
+
29
+ class SettingsRepository(private val dataStore: DataStore<Preferences>) {
30
+
31
+ val theme: Flow<String> = dataStore.data
32
+ .map { prefs -> prefs[SettingsKeys.THEME] ?: "system" }
33
+ .distinctUntilChanged()
34
+
35
+ suspend fun setTheme(value: String) {
36
+ dataStore.edit { prefs -> prefs[SettingsKeys.THEME] = value }
37
+ }
38
+
39
+ suspend fun incrementLaunchCount() {
40
+ dataStore.edit { prefs ->
41
+ prefs[SettingsKeys.LAUNCH_COUNT] = (prefs[SettingsKeys.LAUNCH_COUNT] ?: 0) + 1
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Typed DataStore with a Serializer
48
+
49
+ ```kotlin
50
+ @Serializable
51
+ data class UserSettings(
52
+ val theme: String = "system",
53
+ val notificationsEnabled: Boolean = true,
54
+ val syncIntervalMinutes: Int = 30
55
+ )
56
+
57
+ object UserSettingsSerializer : Serializer<UserSettings> {
58
+ override val defaultValue: UserSettings = UserSettings()
59
+
60
+ override suspend fun readFrom(input: InputStream): UserSettings =
61
+ try {
62
+ Json.decodeFromString(
63
+ UserSettings.serializer(),
64
+ input.readBytes().decodeToString()
65
+ )
66
+ } catch (e: SerializationException) {
67
+ throw CorruptionException("Unable to read UserSettings", e)
68
+ }
69
+
70
+ override suspend fun writeTo(t: UserSettings, output: OutputStream) {
71
+ output.write(
72
+ Json.encodeToString(UserSettings.serializer(), t).encodeToByteArray()
73
+ )
74
+ }
75
+ }
76
+
77
+ val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
78
+ fileName = "user_settings.json",
79
+ serializer = UserSettingsSerializer,
80
+ corruptionHandler = ReplaceFileCorruptionHandler { UserSettings() }
81
+ )
82
+ ```
83
+
84
+ ## Encrypted Typed DataStore
85
+
86
+ The encrypting serializer wraps any inner `Serializer<T>`. It prepends the
87
+ GCM IV to the ciphertext, fails safe to the inner default on any read error,
88
+ and writes plaintext only on debug builds.
89
+
90
+ ```kotlin
91
+ interface CipherProvider {
92
+ fun encryptCipher(): Cipher
93
+ fun decryptCipher(iv: ByteArray): Cipher
94
+ }
95
+
96
+ class EncryptingSerializer<T>(
97
+ private val delegate: Serializer<T>,
98
+ private val cipherProvider: CipherProvider,
99
+ private val allowPlaintextInDebug: Boolean = BuildConfig.DEBUG
100
+ ) : Serializer<T> {
101
+
102
+ override val defaultValue: T = delegate.defaultValue
103
+
104
+ override suspend fun readFrom(input: InputStream): T =
105
+ try {
106
+ val bytes = input.readBytes()
107
+ if (bytes.isEmpty()) return defaultValue
108
+
109
+ val plain = if (allowPlaintextInDebug && bytes[0] == PLAINTEXT_MARKER) {
110
+ bytes.copyOfRange(1, bytes.size)
111
+ } else {
112
+ val ivSize = bytes[0].toInt() and 0xFF
113
+ val iv = bytes.copyOfRange(1, 1 + ivSize)
114
+ val cipherText = bytes.copyOfRange(1 + ivSize, bytes.size)
115
+ cipherProvider.decryptCipher(iv).doFinal(cipherText)
116
+ }
117
+ delegate.readFrom(ByteArrayInputStream(plain))
118
+ } catch (e: Exception) {
119
+ defaultValue
120
+ }
121
+
122
+ override suspend fun writeTo(t: T, output: OutputStream) {
123
+ val plain = ByteArrayOutputStream().also { delegate.writeTo(t, it) }.toByteArray()
124
+
125
+ if (allowPlaintextInDebug) {
126
+ output.write(byteArrayOf(PLAINTEXT_MARKER))
127
+ output.write(plain)
128
+ return
129
+ }
130
+
131
+ val cipher = cipherProvider.encryptCipher()
132
+ val iv = cipher.iv
133
+ val cipherText = cipher.doFinal(plain)
134
+ output.write(byteArrayOf(iv.size.toByte()))
135
+ output.write(iv)
136
+ output.write(cipherText)
137
+ }
138
+
139
+ private companion object {
140
+ const val PLAINTEXT_MARKER: Byte = 0x00
141
+ }
142
+ }
143
+
144
+ fun <T> Context.createEncryptedDataStore(
145
+ name: String,
146
+ serializer: Serializer<T>,
147
+ cipherProvider: CipherProvider
148
+ ): DataStore<T> {
149
+ val encrypting = EncryptingSerializer(serializer, cipherProvider)
150
+ return DataStoreFactory.create(
151
+ serializer = encrypting,
152
+ corruptionHandler = ReplaceFileCorruptionHandler { serializer.defaultValue },
153
+ produceFile = { File(filesDir, "datastore/$name") }
154
+ )
155
+ }
156
+ ```
157
+
158
+ The `CipherProvider` is backed by an AES/GCM Keystore key; that key's creation,
159
+ hierarchy, and invalidation handling belong to `android-security`. The plaintext
160
+ branch is compiled out of release builds because `BuildConfig.DEBUG` is a
161
+ compile-time constant.
162
+
163
+ ## Migrating from SharedPreferences
164
+
165
+ Preferences store, copying named legacy keys once:
166
+
167
+ ```kotlin
168
+ val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
169
+ name = "settings",
170
+ produceMigrations = { context ->
171
+ listOf(
172
+ SharedPreferencesMigration(
173
+ context = context,
174
+ sharedPreferencesName = "legacy_prefs",
175
+ keysToMigrate = setOf("theme", "onboarded")
176
+ )
177
+ )
178
+ }
179
+ )
180
+ ```
181
+
182
+ Typed store, mapping flat legacy values onto the model:
183
+
184
+ ```kotlin
185
+ val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
186
+ fileName = "user_settings.json",
187
+ serializer = UserSettingsSerializer,
188
+ produceMigrations = { context ->
189
+ listOf(
190
+ SharedPreferencesMigration(
191
+ context = context,
192
+ sharedPreferencesName = "legacy_prefs"
193
+ ) { prefs, current ->
194
+ current.copy(
195
+ theme = prefs.getString("theme", current.theme),
196
+ notificationsEnabled = prefs.getBoolean(
197
+ "notifications", current.notificationsEnabled
198
+ )
199
+ )
200
+ }
201
+ )
202
+ }
203
+ )
204
+ ```
205
+
206
+ ## Versioned Data Migrations
207
+
208
+ ```kotlin
209
+ @Serializable
210
+ data class UserSettings(
211
+ val schemaVersion: Int = 2,
212
+ val theme: String = "system",
213
+ val syncIntervalMinutes: Int = 30
214
+ )
215
+
216
+ val v1ToV2 = object : DataMigration<UserSettings> {
217
+ override suspend fun shouldMigrate(currentData: UserSettings): Boolean =
218
+ currentData.schemaVersion < 2
219
+
220
+ override suspend fun migrate(currentData: UserSettings): UserSettings =
221
+ currentData.copy(
222
+ schemaVersion = 2,
223
+ syncIntervalMinutes = currentData.syncIntervalMinutes.coerceAtLeast(15)
224
+ )
225
+
226
+ override suspend fun cleanUp() = Unit
227
+ }
228
+
229
+ val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
230
+ fileName = "user_settings.json",
231
+ serializer = UserSettingsSerializer,
232
+ produceMigrations = { listOf(v1ToV2) }
233
+ )
234
+ ```
235
+
236
+ ## Reading and Writing as Flow
237
+
238
+ ```kotlin
239
+ class SettingsViewModel(
240
+ private val dataStore: DataStore<UserSettings>
241
+ ) : ViewModel() {
242
+
243
+ val theme: StateFlow<String> = dataStore.data
244
+ .catch { e -> if (e is IOException) emit(UserSettings()) else throw e }
245
+ .map { it.theme }
246
+ .distinctUntilChanged()
247
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "system")
248
+
249
+ suspend fun currentSettings(): UserSettings = dataStore.data.first()
250
+
251
+ fun setSyncInterval(minutes: Int) {
252
+ viewModelScope.launch {
253
+ dataStore.updateData { current -> current.copy(syncIntervalMinutes = minutes) }
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ## Testing
260
+
261
+ ```kotlin
262
+ class UserSettingsDataStoreTest {
263
+
264
+ @get:Rule val tmp = TemporaryFolder()
265
+
266
+ private lateinit var scope: TestScope
267
+ private lateinit var dataStore: DataStore<UserSettings>
268
+
269
+ @Before
270
+ fun setUp() {
271
+ scope = TestScope(UnconfinedTestDispatcher())
272
+ dataStore = DataStoreFactory.create(
273
+ serializer = UserSettingsSerializer,
274
+ scope = scope.backgroundScope,
275
+ produceFile = { tmp.newFile("user_settings.json") }
276
+ )
277
+ }
278
+
279
+ @Test
280
+ fun writeThenRead_returnsUpdatedValue() = scope.runTest {
281
+ dataStore.updateData { it.copy(theme = "dark") }
282
+ val result = dataStore.data.first()
283
+ assertEquals("dark", result.theme)
284
+ }
285
+
286
+ @Test
287
+ fun corruptEncryptedFile_failsSafeToDefault() = scope.runTest {
288
+ val file = tmp.newFile("secure.bin").apply { writeBytes(byteArrayOf(9, 9, 9)) }
289
+ val store = DataStoreFactory.create(
290
+ serializer = EncryptingSerializer(UserSettingsSerializer, FakeCipherProvider()),
291
+ scope = scope.backgroundScope,
292
+ produceFile = { file }
293
+ )
294
+ assertEquals(UserSettings(), store.data.first())
295
+ }
296
+ }
297
+ ```
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: android-design-tokens-codegen
3
+ description: "A single-source JSON to typed-Kotlin codegen pipeline for design tokens, UI-test ids, analytics events and deeplink manifests, run as Gradle tasks with a --check CI freshness gate. Use when a design system or cross-platform app needs one source of truth that generates typed code."
4
+ risk: safe
5
+ source: multi-agent-pipeline
6
+ date_added: "2026-09-21"
7
+ ---
8
+
9
+ # Android Design Tokens Codegen
10
+
11
+ A pipeline that treats one or more JSON files as the single source of truth and
12
+ generates typed Kotlin from them at build time: design tokens, UI-test
13
+ identifiers, analytics event definitions, and deeplink manifests. Each generator
14
+ is a thin Gradle task that wraps a generator binary or script, runs before
15
+ compilation, is incremental, and exposes a check mode so CI can fail when the
16
+ committed output is stale. Targets AGP 8.x+, Gradle 8.x+, and KSP-era Kotlin
17
+ (2024-2025).
18
+
19
+ Full, copy-ready Gradle and Kotlin for every section below lives in
20
+ [references/patterns.md](references/patterns.md). Load that file when you need a
21
+ complete task, plugin, or generator example to adapt.
22
+
23
+ ## Contents
24
+
25
+ - [Why single-source codegen](#why-single-source-codegen)
26
+ - [Pipeline shape](#pipeline-shape)
27
+ - [The Gradle task wrapper](#the-gradle-task-wrapper)
28
+ - [Wiring into preBuild](#wiring-into-prebuild)
29
+ - [Check mode and the CI contract](#check-mode-and-the-ci-contract)
30
+ - [Caching and incrementality](#caching-and-incrementality)
31
+ - [The four generated domains](#the-four-generated-domains)
32
+ - [Do's and Don'ts](#dos-and-donts)
33
+ - [Review Checklist](#review-checklist)
34
+ - [Ownership map](#ownership-map)
35
+
36
+ ## Why single-source codegen
37
+
38
+ Constants that describe the same thing in more than one place drift: a color
39
+ value edited in a stylesheet but not in Kotlin, a test tag renamed in the UI but
40
+ not in the test, an analytics event whose name no longer matches the dashboard,
41
+ a deeplink path the app parses one way and the marketing team links another.
42
+ Hand-written constant files are the drift surface.
43
+
44
+ A codegen pipeline removes the surface. The JSON is authored (or exported from a
45
+ design tool) once; a generator turns it into typed Kotlin; nothing downstream is
46
+ hand-edited. Because the output is typed, a removed or renamed token becomes a
47
+ compile error at every call site rather than a silent runtime miss. Reviewers
48
+ diff the JSON, not the generated Kotlin.
49
+
50
+ In a cross-platform shop the same JSON feeds every platform's generator, so the
51
+ guarantee widens: one schema, many typed outputs, no per-platform divergence.
52
+ The mechanism below is platform-neutral in its input and Android-specific only
53
+ in what it emits.
54
+
55
+ ## Pipeline shape
56
+
57
+ ```
58
+ tokens.json ─┐
59
+ test-ids.json ┤ ┌─ generateDesignTokens ─┐
60
+ analytics.json┤─ JSON ─┤─ generateTestIds ├─ typed Kotlin ─ compile
61
+ deeplinks.json┘ (SoT) └─ generate... ┘ (build dir)
62
+ ```
63
+
64
+ Each JSON file is an input; each generator is one Gradle task; each task writes
65
+ Kotlin into a generated-source directory under the build folder, which is added
66
+ to the Kotlin source set. Generated code is not committed unless a check-mode
67
+ contract requires it (see below); either way it is never hand-edited.
68
+
69
+ ## The Gradle task wrapper
70
+
71
+ Keep the generator itself out of the build script. The task's only job is to
72
+ declare inputs and outputs, then invoke a binary or script. A minimal typed task
73
+ looks like this:
74
+
75
+ ```kotlin
76
+ abstract class GenerateFromSchemaTask : DefaultTask() {
77
+ @get:InputFile
78
+ @get:PathSensitive(PathSensitivity.RELATIVE)
79
+ abstract val schema: RegularFileProperty
80
+
81
+ @get:OutputDirectory
82
+ abstract val outputDir: DirectoryProperty
83
+
84
+ @get:Input
85
+ abstract val checkMode: Property<Boolean>
86
+
87
+ @get:Inject
88
+ abstract val execOps: ExecOperations
89
+
90
+ @TaskAction
91
+ fun generate() {
92
+ execOps.exec {
93
+ executable = "codegen"
94
+ args(
95
+ "--schema", schema.get().asFile.absolutePath,
96
+ "--out", outputDir.get().asFile.absolutePath,
97
+ )
98
+ if (checkMode.get()) args("--check")
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Register one instance per domain and point each at its own schema and output
105
+ directory. The generator binary can be a bundled JAR run via `javaexec`, a CLI
106
+ on the path, or a `JavaExec`/`WorkAction`; the wrapper contract is identical.
107
+ Full registration, `WorkAction` isolation, and `javaexec` variants:
108
+ [references/patterns.md#task-wrapper](references/patterns.md#task-wrapper).
109
+
110
+ ## Wiring into preBuild
111
+
112
+ Generation must finish before Kotlin compiles. Depend on the task from
113
+ `preBuild` and register its output as a generated source directory so the
114
+ compiler and the IDE both see it:
115
+
116
+ ```kotlin
117
+ val generateTokens = tasks.register<GenerateFromSchemaTask>("generateDesignTokens") {
118
+ schema.set(layout.projectDirectory.file("schema/tokens.json"))
119
+ outputDir.set(layout.buildDirectory.dir("generated/source/tokens"))
120
+ checkMode.set(providers.gradleProperty("codegen.check").isPresent)
121
+ }
122
+
123
+ androidComponents.onVariants { variant ->
124
+ variant.sources.java?.addGeneratedSourceDirectory(
125
+ generateTokens,
126
+ GenerateFromSchemaTask::outputDir,
127
+ )
128
+ }
129
+
130
+ tasks.named("preBuild").configure { dependsOn(generateTokens) }
131
+ ```
132
+
133
+ `addGeneratedSourceDirectory` is the modern AGP variant API; it wires the task
134
+ dependency and the source set in one call. Prefer it to manually mutating
135
+ `sourceSets`. Full multi-domain wiring and the KSP-consumer ordering note:
136
+ [references/patterns.md#prebuild-wiring](references/patterns.md#prebuild-wiring).
137
+
138
+ ## Check mode and the CI contract
139
+
140
+ The build must fail when someone edits a schema and forgets to regenerate, or
141
+ edits generated Kotlin by hand. Give every generator a `--check` (dry-run) mode:
142
+ it regenerates into a temp location, compares against what is on disk, and exits
143
+ non-zero on any difference instead of writing.
144
+
145
+ Two workable contracts, pick one per repo:
146
+
147
+ 1. Output committed. Generated Kotlin lives in `src` and is tracked. CI runs the
148
+ generators in check mode; a diff means the commit is stale and the build
149
+ fails. Contributors run the plain generate task and commit the result.
150
+ 2. Output not committed. Generated Kotlin lives only in the build dir and is
151
+ produced on every build. CI still runs check mode against a schema-derived
152
+ fixture to prove the generator is deterministic and the schema is valid.
153
+
154
+ Contract 1 is the common choice because it keeps generated code reviewable and
155
+ makes the failure legible ("run ./gradlew generateDesignTokens and commit").
156
+ Wire the check task into the verification lifecycle so it runs in CI:
157
+
158
+ ```kotlin
159
+ val checkCodegen = tasks.register("checkCodegenFresh") {
160
+ dependsOn(generateTokens, generateTestIds, generateAnalytics, generateDeeplinks)
161
+ }
162
+ tasks.named("check").configure { dependsOn(checkCodegen) }
163
+ ```
164
+
165
+ Determinism is a hard requirement for check mode: stable key ordering, fixed
166
+ formatting, no timestamps, no absolute paths in the output. A non-deterministic
167
+ generator makes check mode flap. Full check-mode task, the failure message
168
+ pattern, and a determinism checklist:
169
+ [references/patterns.md#check-mode](references/patterns.md#check-mode).
170
+
171
+ ## Caching and incrementality
172
+
173
+ Declaring `@InputFile` and `@OutputDirectory` (as above) is what lets Gradle
174
+ skip a generator when nothing changed and pull the result from the build cache
175
+ when it did before. With the inputs and outputs declared, the task is up-to-date
176
+ whenever the schema and generator version are unchanged, so a normal build pays
177
+ for generation only on the first run and after a schema edit.
178
+
179
+ Two rules keep this honest:
180
+
181
+ - Every input the output depends on is a declared input. If the generator's own
182
+ version can change the output, declare it as an `@Input` (a version string or
183
+ the generator artifact as an `@Classpath` input) so a generator upgrade
184
+ invalidates the cache instead of serving stale output.
185
+ - Nothing outside `@OutputDirectory` is written. Side-writes defeat
186
+ up-to-date checks and corrupt the cache.
187
+
188
+ Mark the task cacheable and keep `PathSensitivity.RELATIVE` on the schema so the
189
+ cache key is portable across machines and CI checkouts. `@CacheableTask`,
190
+ `@Classpath` generator input, and a `gradle.properties` snippet enabling the
191
+ build and configuration caches: [references/patterns.md#caching](references/patterns.md#caching).
192
+
193
+ ## The four generated domains
194
+
195
+ The same wrapper shape serves four schema families. They differ only in the JSON
196
+ they read and the Kotlin they emit; keep one generator and one task per family
197
+ rather than one mega-generator, so a stale check names the domain that drifted.
198
+
199
+ - Design tokens. Colors, spacing, typography, radii, elevation as typed
200
+ constants or a semantic data class. The generated data class is consumed by
201
+ the Theme layer (see ownership map), not read directly by feature code.
202
+ - UI-test identifiers. One typed constant per test tag, referenced by both the
203
+ `Modifier.testTag` call site in the UI and the test that asserts on it, so a
204
+ rename is a compile error on both sides.
205
+ - Analytics events. Event name plus its typed parameter set, so tracking calls
206
+ cannot pass an unknown event or misspell a parameter key.
207
+ - Deeplink manifests. The route paths and their typed parameters the app parses,
208
+ generated from the same manifest that documents them, so parser and
209
+ documentation cannot diverge.
210
+
211
+ Neutral emit shapes for each domain (a token data class, a test-tag object, a
212
+ sealed analytics event, a deeplink route enum) are in
213
+ [references/patterns.md#generated-shapes](references/patterns.md#generated-shapes).
214
+ Treat those as illustrative structure, not a prescribed token set.
215
+
216
+ ## Do's and Don'ts
217
+
218
+ - Do keep the generator binary versioned and declared as a task input.
219
+ - Do make output deterministic; check mode depends on it.
220
+ - Do run check mode in CI on the `check` lifecycle.
221
+ - Do keep one generator per domain.
222
+ - Don't hand-edit generated Kotlin; edit the schema and regenerate.
223
+ - Don't put generator logic inline in `build.gradle.kts`; wrap a binary.
224
+ - Don't write outside the declared `@OutputDirectory`.
225
+ - Don't emit timestamps, absolute paths, or unstable ordering.
226
+ - Don't let a generator reach the network at build time; the schema is local.
227
+
228
+ ## Review Checklist
229
+
230
+ - [ ] Each generator is a typed task with declared `@InputFile`/`@OutputDirectory`.
231
+ - [ ] Output directory registered via `addGeneratedSourceDirectory`.
232
+ - [ ] `preBuild` depends on every generator.
233
+ - [ ] `--check`/dry-run mode exists and is wired into `check` for CI.
234
+ - [ ] Generator version is a declared task input (cache invalidates on upgrade).
235
+ - [ ] Output is deterministic (stable ordering, no timestamps/paths).
236
+ - [ ] `@CacheableTask` with `PathSensitivity.RELATIVE` on schema inputs.
237
+ - [ ] One generator per domain (tokens, test-ids, analytics, deeplinks).
238
+ - [ ] No hand-edits to generated Kotlin.
239
+
240
+ ## Ownership map
241
+
242
+ This skill owns the single-source-JSON to typed-Kotlin generation mechanism and
243
+ its Gradle/CI wiring. It does not cover:
244
+
245
+ - Consuming the generated semantic token data class in Compose (Theme accessor,
246
+ `CompositionLocal`, dynamic color) -> `compose-components`.
247
+ - General convention-plugin and version-catalog build setup -> `gradle-kotlin-dsl`.
248
+ - Build-failing quality gates in general (lint, detekt, coverage thresholds) ->
249
+ `android-build-quality-gates`.