diagnosing-compose-stability — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited diagnosing-compose-stability (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Compose skips recomposition by comparing parameters. When a parameter is unstable, skipping is disabled — this skill tells Claude how to find out which parameters are unstable and why. The output is a prioritized list of unstable types and non-skippable composables; the fix lives in ../stabilizing-compose-types/SKILL.md.
@TraceRecomposition log shows recomposition counts that exceed the number of meaningful state changes.classes.txt, composables.txt, composables.csv, module.json, or "non-skippable".List<Foo>, LocalDateTime, or a domain type is stable.../stabilizing-compose-types/SKILL.md.Modifier.alpha(state.value)); use ../../recomposition/deferring-state-reads/SKILL.md.derivedStateOf misuse; use ../../recomposition/choosing-derivedstateof/SKILL.md.../enforcing-stability-in-ci/SKILL.md.id("org.jetbrains.kotlin.plugin.compose"). The pre-2.0 kotlinCompilerExtensionVersion flow is obsolete.org.jetbrains.kotlin.plugin.compose Gradle plugin (Kotlin 2.0+). The composeCompiler { … } extension is owned by that plugin; AGP version is incidental.// app/build.gradle.kts (or any compose module)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
composeCompiler {
// Only emit reports for release to avoid Live Literals noise.
val isReleaseBuild = providers.gradleProperty("composeCompilerReports").orNull == "true"
if (isReleaseBuild) {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}Or the always-on form:
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
// Optional — opt mutable third-party types into stability:
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("stability_config.conf")
)
}./gradlew :app:assembleRelease -PcomposeCompilerReports=trueFor a library module: ./gradlew :feature-feed:assembleRelease. The release flavor is required — see RIGHT/WRONG below.
<module>/build/compose_compiler/:app/build/compose_compiler/
├── app_release-classes.txt # per-class stability
├── app_release-composables.txt # per-composable signatures
├── app_release-composables.csv # CSV mirror of the above (CI-friendly)
└── app_release-module.json # aggregate countsIf any file is missing, the plugin did not run for that variant — re-check that composeCompiler { reportsDestination = ... } is on the right module and that the build was release.
restartable lines that are not followed by skippable. Each one is a recomposition entry point that cannot be skipped. Inside each block, read the per-parameter prefix: stable, unstable, @static, @dynamic. Any unstable parameter blocks skipping. See references/reading-composables-txt.md for the full grammar.unstable in composables.txt, find its declaration in classes.txt. The line tells you whether a var field, a generic parameter, or an unstable nested type is the cause. The runtime stable class Box { stable val value: T } shape means "this class is stable iff the runtime $stable: Int field of the substituted T says so" — see references/reading-classes-txt.md.LazyColumn item is critical. Cross-reference with measurement (@TraceRecomposition from skydoves/compose-stability-analyzer, or Macrobenchmark FrameTimingMetric) before fixing — see ../../measurement/tracing-recompositions-at-runtime/SKILL.md.../stabilizing-compose-types/SKILL.md.The compiler reports surface the cause directly inside the function signature. Walk the developer through this real shape.
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int,
unstable snacks: List<Snack>, // <-- blocks skipping
stable onSnackClick: Function1<Long, Unit>,
)Diagnosis script:
restartable but NOT prefixed with skippable. Therefore it always recomposes when its parent does.unstable snacks: List<Snack> parameter. kotlin.collections.List is an interface; the compiler cannot prove its implementations are immutable.classes.txt and find Snack. If Snack itself is unstable, fix the data class first; if it is stable then only the List wrapper is the problem.List<Snack> with kotlinx.collections.immutable.ImmutableList<Snack>, or add kotlin.collections.* to stability_config.conf if the developer is comfortable with that contract.runtime meansruntime stable class Box {
stable val value: T
}This is not unstable. The compiler emits a synthetic $stable: Int field at runtime and queries it during composition. The class is stable iff the substituted T reports stable. DO NOT annotate runtime classes with @Stable to "promote" them — the runtime check is the correct mechanism.
# WRONG
./gradlew :app:assembleDebug
# WRONG because: debug enables Live Literals; constant 0 dp becomes a getter, every literal looks dynamic, and counts in module.json drift versus what ships to users.# RIGHT
./gradlew :app:assembleRelease -PcomposeCompilerReports=true// WRONG — reading composables.txt without checking the leading flags
fun MyScreen(...)
// WRONG because: skipping the `restartable`/`skippable` prefix discards the only data point that determines whether unstable params actually cost anything.// RIGHT — every diagnosis quotes the full prefix and per-param annotations
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MyScreen(
stable user: User,
stable onClick: Function0<Unit>,
)If build/compose_compiler/ is missing or empty after a release build:
org.jetbrains.kotlin.plugin.compose plugin is applied to this module — the extension is per-module../gradlew :app:clean :app:assembleRelease.reportsDestination is set inside a composeCompiler { } block, not the legacy kotlinOptions freeCompilerArgs flow.composables.txt per-parameter annotations (stable/unstable/@static/@dynamic), not just the function name. The flag prefix is the diagnosis.classes.txt to identify the root cause (a var, a generic, an unstable field type, or an interface).@Stable based on a report alone — that decision belongs to the fix skill, which evaluates the contract.composables.csv line counts and call it done. The counts are a regression sentinel; the per-line annotations are the actual diagnosis.../enforcing-stability-in-ci/SKILL.md (skydoves compose-stability-analyzer plugin or the community ComposeGuard plugin) so regressions surface on PR review.module.json per release so future regressions are visible by diff../gradlew :app:assembleRelease (or module-specific) completes successfully with the org.jetbrains.kotlin.plugin.compose plugin applied.<module>_release-classes.txt, <module>_release-composables.txt, <module>_release-composables.csv, <module>_release-module.json.composables.txt, OR a confirmed-zero count from module.json justifying that no fix is needed.classes.txt so the fix skill receives a concrete root cause (a var, a List, a LocalDateTime, etc.).runtime stable class … is not a problem — it is the compiler's correct lazy-stability emission.references/reading-classes-txt.md — full grammar for classes.txt with worked examples (stable / unstable / runtime / generic).references/reading-composables-txt.md — full grammar for composables.txt including restartable, skippable, readonly, scheme, and per-parameter stable / unstable / @static / @dynamic.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.