Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Full release history for the Compose Skill Suite. The newest release is summarised under **What's new** in the [README](./README.md).

### 4.3.0 — 2026-07-06

**`jetpack-compose-audit` only — cross-phase back-write detection + false-lead guard.**

- **Cross-phase back-writes — axis 3 (new detector).** The audit already deducted for same-body backwards writes; it now also catches the subtler cross-phase shape — a **layout-phase** write (`onSizeChanged` / `onGloballyPositioned` / `onPlaced`) into state a sibling reads in composition. `search-playbook.md` §3 gains the heuristic, `scoring.md` the matching Performance deduction (Critical when the loop is per-frame or spans reused lazy items), `diagnostics.md` a step-7 triage. Findings cite the writer and reader `file:line`.
- **Composition-phase self-invalidation (related check).** A `SnapshotStateMap` / `SnapshotStateList` (including `toMutableStateList()` / `toMutableStateMap()` builders) mutated inside a `@Composable` body that also reads it. This is *not* cross-phase — write and read are both in composition — and is deduped against the same-body backwards-write rule so it never double-counts.
- **False-lead guard (scoring correctness).** `scoring.md` Performance gains a **Do Not Credit — False Leads** table: plausible "recomposition fixes" that change nothing (`remember(index)` on pure functions, identity caches for derived maps, layout modifiers on both measured and sibling rows, `Exactly(1)` forced on both rows, and — under Strong Skipping — manually `remember`-ing a callback that the compiler already auto-memoizes). The auditor no longer rewards these and will not emit them in `Prioritized Fixes` — it verifies against runtime counts / compiler reports instead of the presence of the pattern.
- **Attribution.** The three-axis framing and the false-lead cases are adapted (reworded, re-cited against `developer.android.com`) from [`chrisbanes/skills`](https://github.com/chrisbanes/skills) `compose-recomposition-performance` (Apache-2.0). Axes 1 (stability/skipping) and 2 (deferred reads) were already covered by this suite; this release imports only axis 3 and the false-lead guard.
- **Versions.** `jetpack-compose-audit` → `4.3.0`. `compose-agent` unchanged at `4.2.1`. Note: the 4.2.2 Nav3 work was docs-only and never bumped the manifests, so `jetpack-compose-audit`'s `plugin.json` / SKILL version moves straight from `4.2.0` to `4.3.0` here — 4.2.2 lives in the changelog and shipped content but not in a version string.

### 4.2.2 — 2026-06-30

**`jetpack-compose-audit` only — Navigation 3 audit patterns.**
Expand All @@ -10,7 +20,7 @@ Full release history for the Compose Skill Suite. The newest release is summaris
- **Scoring table.** `scoring.md` maps all Nav3 findings to existing categories: State Management (anonymous keys, ViewModel ownership, `ResultEventBus` risk) and Side Effects (composition-body navigation, missing `dropUnlessResumed`); `@Composable`-in-NavKey and Nav2+Nav3 conflict promoted to Critical Findings.
- **Canonical sources.** Added `https://developer.android.com/guide/navigation/navigation-3` and `https://github.com/android/nav3-recipes` to `canonical-sources.md`.
- **SKILL.md trigger.** Step 5 now directs auditors to run playbook section 2b whenever `rememberNavBackStack`, `NavDisplay`, or `NavKey` is detected.
- **Versions.** `jetpack-compose-audit` → `4.2.2`. `compose-agent` unchanged at `4.2.1`.
- **Versions.** `jetpack-compose-audit` → `4.2.2` (changelog/content only; the `plugin.json` / SKILL version strings stayed at `4.2.0` and are bumped to `4.3.0` in the next release). `compose-agent` unchanged at `4.2.1`.

### 4.2.1 — 2026-06-29

Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude%20Code-plugin-111827">
</p>

**`compose-agent` 4.2.1 · 2026-06-29** — Navigation 3 "when to use" decision table in `navigation.md` (workflow entry point: Nav3 vs Nav2 type-safe vs plain state, adaptive scene strategies; the back stack is mutated in the route, not a screen ViewModel; results via Nav3's `ResultEventBus`). Verified against the official `android/nav3-recipes` samples. `jetpack-compose-audit` stays at `4.2.0`.
**`jetpack-compose-audit` 4.3.0 · 2026-07-06** — Cross-phase back-write detection (axis 3: layout callbacks writing state read in composition), a related composition-phase self-invalidation check (snapshot collections mutated in a composable body), and a **False Leads** scoring guard so the auditor stops crediting no-op "recomposition fixes." Adapted from [`chrisbanes/skills`](https://github.com/chrisbanes/skills) (Apache-2.0). `compose-agent` stays at `4.2.1`.

**Version 4.2.0 · 2026-06-17** — Paging 3 in Compose: new `paging.md` reference (LLM guardrails, not API tour), audit hooks under existing Performance/State categories, planning doc at [`docs/paging-skill-plan.md`](./docs/paging-skill-plan.md). Validated through multi-agent cross-review. Both skills ship as `4.2.0`.

Expand All @@ -29,6 +29,18 @@ Authored and cross-reviewed with every frontier model — Claude Opus 4.8, GPT-5

## What's new

### 4.3.0 — 2026-07-06

**`jetpack-compose-audit` — cross-phase back-writes + false-lead guard.**

- **Cross-phase back-write detector (axis 3).** Beyond same-body backwards writes, the audit now flags a **layout-phase** write into state an earlier composition read: `onSizeChanged` / `onGloballyPositioned` / `onPlaced` writing state a sibling reads in composition. New heuristic in [`search-playbook.md`](./skills/jetpack-compose-audit/references/search-playbook.md), matching Performance deduction in [`scoring.md`](./skills/jetpack-compose-audit/references/scoring.md), triage step 7 in [`diagnostics.md`](./skills/jetpack-compose-audit/references/diagnostics.md). Findings cite writer and reader `file:line`.
- **Composition-phase self-invalidation (related, not cross-phase).** A `SnapshotStateMap` / `SnapshotStateList` mutated inside a `@Composable` body that also reads it. Deduped against the same-body backwards-write rule so it never double-counts.
- **False Leads guard.** New **Do Not Credit — False Leads** table in `scoring.md`: plausible recomposition "fixes" that change nothing. The auditor no longer rewards them and will not suggest them in `Prioritized Fixes`.
- **Attribution.** Axis 3 and the false-lead cases adapted (reworded, re-cited against `developer.android.com`) from [`chrisbanes/skills`](https://github.com/chrisbanes/skills) `compose-recomposition-performance` (Apache-2.0). Axes 1–2 were already covered by this suite.
- **Versions.** `jetpack-compose-audit` → `4.3.0`. `compose-agent` unchanged at `4.2.1`.

For release detail, see [`docs/release-notes-4.3.0.md`](./docs/release-notes-4.3.0.md). Full history: [CHANGELOG.md](./CHANGELOG.md).

### 4.2.1 — 2026-06-29

**`compose-agent` — Navigation 3 decision table.**
Expand Down Expand Up @@ -68,7 +80,7 @@ Four categories, weighted for an app repo. Each scored `0-10`; overall on `0-100

| Category | Weight | What it covers |
|----------|--------|----------------|
| **Performance** | 35% | Work in composition, lazy-list keys, state-read timing, stability, Strong Skipping, backwards writes, **animation phase correctness**, baseline profiles |
| **Performance** | 35% | Work in composition, lazy-list keys, state-read timing, stability, Strong Skipping, backwards writes, **cross-phase back-writes**, **animation phase correctness**, baseline profiles |
| **State management** | 25% | Hoisting, single source of truth, `rememberSaveable`, lifecycle-aware collection, observable collections, ViewModel placement, type-safe navigation |
| **Side effects** | 20% | Effect API choice, keys, stale captures, cleanup, composition-time work, **animation driving via `LaunchedEffect`** |
| **Composable API quality** | 20% | Modifier conventions, parameter order, slot APIs, `CompositionLocal` usage, `Modifier.Node`, **`animationSpec` exposure**, `@Preview` coverage, hardcoded strings / magic numbers |
Expand Down
41 changes: 41 additions & 0 deletions docs/release-notes-4.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Release notes — 4.3.0 (2026-07-06)

**`jetpack-compose-audit` only.** `compose-agent` unchanged at `4.2.1`.

Two additions to the Performance category, plus a scoring-correctness guard. The suite already covered axes 1 (stability/skipping) and 2 (deferred reads) of the recomposition problem; this release adds axis 3 and stops the auditor from crediting no-op "fixes".

## Cross-phase back-writes (axis 3)

A **layout-phase** callback (`onSizeChanged` / `onGloballyPositioned` / `onPlaced`) that writes snapshot state a sibling reads in **composition** creates a measure → write → recompose loop, often per-frame during resize/scroll. The audit already flagged same-body backwards writes; it now catches this cross-phase shape too.

- `search-playbook.md` — new **Cross-Phase Back-Write & Composition Self-Invalidation Heuristic**: gather layout-callback writers, confirm a composition-phase reader, cite both `file:line`.
- `scoring.md` — Performance deduction; promoted to a Critical Finding when the loop is per-frame or spans reused lazy items. A matching Reward bullet credits measured values kept in layout/draw (`Modifier.layout { }` / `Modifier.drawBehind { }`).
- `diagnostics.md` — Quick Triage Recipe step 7 surfaces candidates before rubric reading, scoped to the files that actually hold snapshot collections.

## Composition-phase self-invalidation (related check)

A `SnapshotStateMap` / `SnapshotStateList` (including the `toMutableStateList()` / `toMutableStateMap()` builders) mutated inside a `@Composable` body that also reads it. This is **not** cross-phase — write and read are both in composition — and is **deduped** against the same-body backwards-write rule so it is scored once, never double-counted.

## Do Not Credit — False Leads

A guard table in `scoring.md` Performance: plausible "recomposition fixes" that change nothing, so the auditor neither rewards them nor emits them in `Prioritized Fixes`.

- `remember(index) { … }` around a pure, cheap function of its own key.
- Identity/instance caches for read-only derived maps (use `remember(keys)`).
- Layout modifiers on both the measured and the sibling row (the sibling still reads size in composition).
- `assertRecompositionCount(Exactly(1))` forced on both rows when one correctly recomposes 0 times.
- Under **Strong Skipping**, manually `remember`-ing a callback the compiler already auto-memoizes — that lever only applies SSM-off, on `@NonSkippableComposable` / `@DontMemoize` paths, or when the lambda captures an unstable value.

The guard verifies against runtime recomposition counts / compiler reports rather than the presence of a pattern.

## Evals

`evals/evals.json` cases 12–14 lock in the new behavior: cross-phase layout→composition back-write (12), false leads that must not be credited or suggested under SSM (13), and composition-phase snapshot self-invalidation (14). Audit-only — `compose-agent` has no cross-phase / false-lead guidance yet (tracked as follow-up).

## Attribution

Axis 3 and the false-lead cases are adapted — reworded and re-cited against `developer.android.com` — from [`chrisbanes/skills`](https://github.com/chrisbanes/skills) `compose-recomposition-performance` (Apache-2.0).

## Versions

`jetpack-compose-audit` → **4.3.0** (SKILL + both `plugin.json`). The 4.2.2 Nav3 work was changelog/content only and never bumped the manifests, so the version string moves from `4.2.0` straight to `4.3.0`.
2 changes: 1 addition & 1 deletion skills/jetpack-compose-audit/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jetpack-compose-audit",
"version": "4.2.0",
"version": "4.3.0",
"description": "Strict, evidence-based audit for Android Jetpack Compose repositories. Produces a 0-100 score, per-category 0-10 scores, a COMPOSE-AUDIT-REPORT.md with every deduction cited against developer.android.com, and coverage notes for animation performance, paging list correctness, UI tests, focus/keyboard, Compose Multiplatform, and Android launch UX surfaces.",
"author": {
"name": "Ivan Morgillo"
Expand Down
2 changes: 1 addition & 1 deletion skills/jetpack-compose-audit/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jetpack-compose-audit",
"version": "4.2.0",
"version": "4.3.0",
"description": "Strict, evidence-based audit for Android Jetpack Compose repositories. Produces a 0-100 score, per-category 0-10 scores, a COMPOSE-AUDIT-REPORT.md with every deduction cited against developer.android.com, and coverage notes for animation performance, paging list correctness, UI tests, focus/keyboard, Compose Multiplatform, and Android launch UX surfaces.",
"author": {
"name": "Ivan Morgillo"
Expand Down
4 changes: 2 additions & 2 deletions skills/jetpack-compose-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ argument-hint: "[repo path or module path]"

This skill audits Android Jetpack Compose repositories with a strict, evidence-based report.

**Skill version:** 4.2.0 — released 2026-06-17. **Compose track:** Kotlin 2.0.20+ / Compose Compiler 1.5.4+ (Strong Skipping Mode default). See the README changelog for what changed.
**Skill version:** 4.3.0 — released 2026-07-06. **Compose track:** Kotlin 2.0.20+ / Compose Compiler 1.5.4+ (Strong Skipping Mode default). See the README changelog for what changed.

It is intentionally focused on four categories:

Expand Down Expand Up @@ -206,7 +206,7 @@ Focus on:
- paging list keys and `LazyPagingItems` misuse when `collectAsLazyPagingItems` is present
- bad state-read timing
- unstable or overly broad reads
- backwards writes
- backwards writes, including cross-phase back-writes (layout callback writing state read in composition) and snapshot-collection self-invalidation in a composable body
- animation phase correctness (per-frame values deferred to layout/draw via lambda modifiers, `Animatable` held in `remember`, `rememberInfiniteTransition` scoped so it stops offscreen)
- obvious release-performance hygiene where visible

Expand Down
41 changes: 40 additions & 1 deletion skills/jetpack-compose-audit/evals/evals.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"skill_name": "jetpack-compose-audit",
"description": "Acceptance evals for the API-hygiene, effect-correctness, cancellation, and state-boundary behavior introduced in 4.1.x, plus Paging 3 in Compose list/load-state behavior added in 4.2.0 (cases 10-11). These are the audit/review acceptance criteria the skill must preserve; they also back compose-agent review behavior. Run a model against each prompt and check it satisfies every expectation.",
"description": "Acceptance evals for the API-hygiene, effect-correctness, cancellation, and state-boundary behavior introduced in 4.1.x, plus Paging 3 in Compose list/load-state behavior added in 4.2.0 (cases 10-11), plus cross-phase back-write detection and the false-lead scoring guard added in 4.3.0 (cases 12-13, audit-only — compose-agent has no cross-phase / false-lead guidance yet). These are the audit/review acceptance criteria the skill must preserve; the earlier cases also back compose-agent review behavior. Run a model against each prompt and check it satisfies every expectation.",
"evals": [
{
"id": 0,
Expand Down Expand Up @@ -147,6 +147,45 @@
"Recommend gating full-screen error on `itemCount == 0` and surfacing refresh errors that arrive with content as a snackbar / inline retry.",
"Credit the correct stable `itemKey` already present instead of re-flagging it."
]
},
{
"id": 12,
"prompt": "Audit this Compose layout for recomposition cost:\n@Composable fun PriceRow(label: String, price: String) {\n var labelWidth by remember { mutableIntStateOf(0) }\n Row {\n Text(label, Modifier.onSizeChanged { labelWidth = it.width })\n Spacer(Modifier.width(with(LocalDensity.current) { labelWidth.toDp() }))\n Text(price)\n }\n}",
"expected_output": "An audit flagging the cross-phase back-write: onSizeChanged (layout) writes labelWidth, which the Spacer reads in composition, forcing measure -> write -> recompose. Recommends consuming the measured width in the layout/draw phase (Modifier.layout {} / a custom Layout / SubcomposeLayout) instead of round-tripping through composition-read state.",
"files": [],
"expectations": [
"Flag `onSizeChanged` writing `labelWidth` that is read in composition (by the `Spacer`) as a cross-phase (axis 3) back-write, not a same-body backwards write.",
"Identify both sides: the layout-phase writer (`onSizeChanged`) and the composition-phase reader (`labelWidth.toDp()` in the `Spacer`).",
"Recommend keeping the measured value in layout/draw — `Modifier.layout { }`, a custom `Layout`, or `SubcomposeLayout` — rather than a composition-read state hop. Do not accept `BoxWithConstraints`, which exposes parent constraints, not a sibling's measured width.",
"Do not misclassify it as a stability / unstable-parameter problem.",
"Treat it as Critical if the row is a reused lazy item or the size changes per frame."
]
},
{
"id": 13,
"prompt": "This screen targets Kotlin 2.1 / Compose Compiler with Strong Skipping on by default. A teammate says they 'optimized recomposition' with the two changes below. Audit whether these are real wins and whether to keep them:\n@Composable fun Rows(items: List<Row>, onOpen: (Row) -> Unit) {\n val stableOnOpen = remember(onOpen) { onOpen }\n LazyColumn { itemsIndexed(items) { index, row ->\n val isFirst = remember(index) { index == 0 }\n RowItem(row, isFirst, onClick = { stableOnOpen(row) })\n } }\n}",
"expected_output": "An audit that does not credit either change as a recomposition win under Strong Skipping: SSM already auto-memoizes lambdas so wrapping onOpen in remember is a no-op, and remember(index) around the pure `index == 0` saves no work. Neither should appear in Prioritized Fixes; any real advice points at runtime recomposition counts / compiler reports.",
"files": [],
"expectations": [
"Do not credit `remember(onOpen) { onOpen }` as a stabilization win — under Strong Skipping lambdas are auto-memoized, so it is a no-op.",
"Do not credit `remember(index) { index == 0 }` — it wraps a pure, cheap expression keyed on its own input and saves nothing.",
"Do not list either pattern in Prioritized Fixes, and do not deduct for removing them.",
"If recommending anything, cite runtime recomposition counts or compiler reports as proof rather than the presence of the pattern.",
"Note that the `remember`-the-callback advice only applies with Strong Skipping OFF or on `@NonSkippableComposable` / `@DontMemoize` paths."
]
},
{
"id": 14,
"prompt": "Audit this overlay for recomposition cost:\n@Composable fun Overlay(rows: List<Row>) {\n val heights = remember { mutableStateMapOf<String, Int>() }\n rows.forEach { heights[it.id] = it.baseHeight }\n Column {\n rows.forEach { row ->\n Box(Modifier.height((heights[row.id] ?: 0).dp)) { RowContent(row) }\n }\n }\n}",
"expected_output": "An audit flagging the mutableStateMapOf mutated in the composable body (heights[it.id] = ...) that the same composition then reads, as composition-phase self-invalidation — scored once, not double-counted with the same-body backwards-write rule, and explicitly not a cross-phase (axis 3) back-write. Recommends deriving from params (remember(rows) { ... }) or moving the mutation into a state holder, rather than mutating snapshot state during composition.",
"files": [],
"expectations": [
"Flag the `mutableStateMapOf` mutation (`heights[it.id] = ...`) in the composable body that the same composition reads as composition-phase self-invalidation.",
"Do NOT label it a cross-phase (axis 3) back-write — both the write and the read happen in composition.",
"Score it once — do not additionally deduct under the same-body backwards-write rule for the same code.",
"Recommend deriving the map from params (`remember(rows) { ... }`) or moving the mutation to a state holder / event, not mutating snapshot state during composition.",
"Do not flag this as an unstable-parameter / stability problem."
]
}
]
}
Loading
Loading