svharness 0.8.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.
- package/README.md +531 -0
- package/bin/cli.js +3 -0
- package/dist/adapters/_frontmatter.js +24 -0
- package/dist/adapters/claude-code.js +12 -0
- package/dist/adapters/codechat.js +12 -0
- package/dist/adapters/cursor.js +19 -0
- package/dist/adapters/generic.js +19 -0
- package/dist/adapters/index.js +26 -0
- package/dist/adapters/qoder.js +12 -0
- package/dist/commands/apply.js +272 -0
- package/dist/commands/init.js +420 -0
- package/dist/core/agent-injector.js +192 -0
- package/dist/core/next-steps.js +91 -0
- package/dist/core/render-meta.js +81 -0
- package/dist/core/repomix-pack.js +54 -0
- package/dist/core/scaffold.js +93 -0
- package/dist/core/state.js +80 -0
- package/dist/index.js +239 -0
- package/dist/types.js +5 -0
- package/dist/utils/baseline-copy.js +591 -0
- package/dist/utils/baseline-defaults.js +106 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/validate-args.js +132 -0
- package/dist/utils/version.js +23 -0
- package/dist/wiki/abort.js +30 -0
- package/dist/wiki/config.js +79 -0
- package/dist/wiki/defaults.js +16 -0
- package/dist/wiki/envLoader.js +78 -0
- package/dist/wiki/index.js +29 -0
- package/dist/wiki/openaiCompat.js +219 -0
- package/dist/wiki/repowikiCanonicalSections.js +67 -0
- package/dist/wiki/repowikiCheckpoint.js +106 -0
- package/dist/wiki/repowikiConfig.js +9 -0
- package/dist/wiki/repowikiGit.js +73 -0
- package/dist/wiki/repowikiIndexer.js +824 -0
- package/dist/wiki/repowikiMarkdownPost.js +123 -0
- package/dist/wiki/repowikiMetadataContent.js +64 -0
- package/dist/wiki/repowikiMetadataJson.js +15 -0
- package/dist/wiki/repowikiScanner.js +156 -0
- package/dist/wiki/repowikiStructureNav.js +286 -0
- package/dist/wiki/repowikiStructureNormalize.js +218 -0
- package/dist/wiki/wikiStructureXml.js +316 -0
- package/dist/wiki/wikiTasksWriter.js +127 -0
- package/package.json +57 -0
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
- package/templates/_shared/meta/AGENTS.md.ejs +53 -0
- package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
- package/templates/_shared/meta/README.md.ejs +51 -0
- package/templates/_shared/meta/VERSION.ejs +1 -0
- package/templates/_shared/meta/harness.yaml.ejs +52 -0
- package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
- package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
- package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
- package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
- package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +168 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +183 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
- package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
- package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
- package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
- package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
- package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-no-props-drilling.md +43 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Search Playbook
|
|
2
|
+
|
|
3
|
+
Use this playbook to gather evidence quickly, then verify by reading representative files.
|
|
4
|
+
|
|
5
|
+
**Always scope ripgrep to Kotlin sources** with `-g '*.kt' -g '*.kts'` (or the `Grep` tool's `glob` parameter / `type: kotlin`) — the regexes below are written for Kotlin and produce noise on JVM/Java/JS files. For build-config searches, use `-g '*.gradle*' -g '*.toml'` instead.
|
|
6
|
+
|
|
7
|
+
## 1. Map The Repo First
|
|
8
|
+
|
|
9
|
+
Before category scoring, locate:
|
|
10
|
+
|
|
11
|
+
- Gradle settings and module declarations
|
|
12
|
+
- Android app and feature modules
|
|
13
|
+
- likely Compose source folders
|
|
14
|
+
- shared component or design-system directories
|
|
15
|
+
- theme packages
|
|
16
|
+
- screen packages
|
|
17
|
+
- ViewModels and state holders
|
|
18
|
+
- test and preview directories
|
|
19
|
+
|
|
20
|
+
Useful targets often include:
|
|
21
|
+
|
|
22
|
+
- `app/`
|
|
23
|
+
- `feature*/`
|
|
24
|
+
- `core/ui/`
|
|
25
|
+
- `designsystem/`
|
|
26
|
+
- `ui/components/`
|
|
27
|
+
- `ui/screens/`
|
|
28
|
+
- `presentation/`
|
|
29
|
+
|
|
30
|
+
## 2. Confirm Compose Surface Area
|
|
31
|
+
|
|
32
|
+
Search for:
|
|
33
|
+
|
|
34
|
+
- `@Composable`
|
|
35
|
+
- `MaterialTheme`
|
|
36
|
+
- `setContent`
|
|
37
|
+
- `ComposeView`
|
|
38
|
+
- `remember`
|
|
39
|
+
- `mutableStateOf`
|
|
40
|
+
|
|
41
|
+
If Compose usage is sparse, state that clearly in the report and reduce confidence.
|
|
42
|
+
|
|
43
|
+
## 3. Performance Checks
|
|
44
|
+
|
|
45
|
+
### Search For
|
|
46
|
+
|
|
47
|
+
- `remember\(`
|
|
48
|
+
- `derivedStateOf`
|
|
49
|
+
- `LazyColumn|LazyRow|LazyVerticalGrid|LazyHorizontalGrid|LazyVerticalStaggeredGrid|LazyHorizontalStaggeredGrid`
|
|
50
|
+
- `items\(`
|
|
51
|
+
- `itemsIndexed\(`
|
|
52
|
+
- `rememberLazyListState`
|
|
53
|
+
- `animate`
|
|
54
|
+
- `offset\(`
|
|
55
|
+
- `drawBehind`
|
|
56
|
+
- `sortedWith|sortedBy|filter|map|associate|groupBy`
|
|
57
|
+
- stability annotations and immutable collections: `@Stable`, `@Immutable`, `kotlinx\.collections\.immutable`, `ImmutableList`, `PersistentList`, `persistentListOf`, `compose_compiler_config`
|
|
58
|
+
- skipping opt-outs: `@NonSkippableComposable`, `@DontMemoize`
|
|
59
|
+
- typed state factories: `mutableIntStateOf`, `mutableLongStateOf`, `mutableFloatStateOf`, `mutableDoubleStateOf`
|
|
60
|
+
- composition-marker annotations: `@ReadOnlyComposable`, `@NonRestartableComposable`
|
|
61
|
+
- baseline / profile setup: `baselineProfile`, `ProfileInstaller`, `androidx\.profileinstaller`, `baseline-prof\.txt`
|
|
62
|
+
- deprecated/legacy APIs: `accompanist-pager`, `accompanist-swiperefresh`, `accompanist-flowlayout`, `accompanist-systemuicontroller`, `animateItemPlacement\(`
|
|
63
|
+
- config-derived reads inside `remember {}`: `LocalConfiguration`, `LocalDensity`, `LocalLayoutDirection`
|
|
64
|
+
- `\.indexOf\(|\.lastIndexOf\(|\.indexOfFirst\s*\{` inside lazy item factories
|
|
65
|
+
- `Canvas\s*\(` and `Spacer\s*\(` — check each hit for an explicit `size` / `height` / `aspectRatio` on the modifier; bare `fillMaxSize()` on a drawing surface can enter draw with `Size.Zero`
|
|
66
|
+
- `ReportDrawnWhen\s*\{` — positive signal for startup metrics
|
|
67
|
+
- `enableEdgeToEdge\s*\(` — positive signal; also confirms the project is not reaching for the deprecated `accompanist-systemuicontroller`
|
|
68
|
+
|
|
69
|
+
### Red Flags To Verify
|
|
70
|
+
|
|
71
|
+
- expensive list transforms inside composable bodies
|
|
72
|
+
- expensive work passed directly to `items(...)`
|
|
73
|
+
- `Lazy*` items without `key =` where item identity can move or reorder — see the lazy-list-without-key heuristic at the bottom of this section
|
|
74
|
+
- scroll or animation state read high in the tree
|
|
75
|
+
- fast-changing values passed to non-lambda modifiers when a layout/draw-phase alternative exists
|
|
76
|
+
- backwards writes — *writing to state that has already been read in the same composition body* (this is the precise definition; reading after writing is fine)
|
|
77
|
+
- `mutableStateOf<Int>` / `<Long>` / `<Float>` / `<Double>` — the typed factories avoid boxing
|
|
78
|
+
- raw `List`/`Map`/`Set` parameters on widely reused composables when `kotlinx.collections.immutable` is already a dependency
|
|
79
|
+
- `@NonSkippableComposable` / `@DontMemoize` without a justifying comment
|
|
80
|
+
- `remember { … }` whose body reads `LocalConfiguration` / `LocalDensity` / `LocalLayoutDirection` without listing that source as a key — cached value goes stale on rotation/foldable/font-scale changes
|
|
81
|
+
- `indexOf(...)` / `lastIndexOf(...)` / `indexOfFirst { ... }` called inside a `LazyListScope` item factory — O(n²) scrolling cost and crash risk if identity moves; prefer `itemsIndexed`
|
|
82
|
+
- `animateItemPlacement()` usage on Compose 1.7+ — replaced by `Modifier.animateItem()`
|
|
83
|
+
- Accompanist libraries where first-party replacements exist: `accompanist-pager` → `HorizontalPager` / `VerticalPager`; `accompanist-swiperefresh` → `PullToRefreshBox`; `accompanist-flowlayout` → `FlowRow` / `FlowColumn`; `accompanist-systemuicontroller` → `enableEdgeToEdge()`
|
|
84
|
+
|
|
85
|
+
### Positive Signals
|
|
86
|
+
|
|
87
|
+
- `remember(keys)` around expensive calculations
|
|
88
|
+
- `derivedStateOf` used for scroll-triggered UI thresholds
|
|
89
|
+
- lambda modifiers such as `Modifier.offset { ... }`, `Modifier.graphicsLayer { ... }`, `Modifier.drawBehind { ... }`
|
|
90
|
+
- draw/layout phase reads instead of full recomposition for rapidly changing values
|
|
91
|
+
- `@Stable` / `@Immutable` on data classes used as composable params
|
|
92
|
+
- `ImmutableList` / `PersistentList` for collection params
|
|
93
|
+
- `compose_compiler_config.conf` to mark third-party types stable
|
|
94
|
+
- baseline-profile modules or profile installer setup when app maturity suggests it matters
|
|
95
|
+
|
|
96
|
+
### Lazy-List-Without-Key Heuristic
|
|
97
|
+
|
|
98
|
+
There's no clean single regex for this. Use a two-step approach:
|
|
99
|
+
|
|
100
|
+
1. Find files that use a lazy layout: `rg -l 'Lazy(Column|Row|VerticalGrid|HorizontalGrid|VerticalStaggeredGrid|HorizontalStaggeredGrid)\b' -g '*.kt'`
|
|
101
|
+
2. Within each file, look for `items(` / `itemsIndexed(` invocations that omit `key =`. Useful multiline pattern: `rg -U --multiline-dotall 'items(?:Indexed)?\([^)]*\)' -g '*.kt'` and read each hit for a `key =` argument.
|
|
102
|
+
|
|
103
|
+
Manually verify before deducting — `items(count: Int)` overloads and small static lists that never reorder are not bugs.
|
|
104
|
+
|
|
105
|
+
### Duplicate-Lazy-Key Heuristic
|
|
106
|
+
|
|
107
|
+
Compose throws `IllegalArgumentException: Key ... was already used` when a `Lazy*` layout sees two items with the same key. Root causes in production code: backend returning duplicate IDs, merging streams (e.g. WebSocket reconnect), or `Pager` + `LazyColumn` combinations where the same item appears in overlapping pages.
|
|
108
|
+
|
|
109
|
+
There is no clean regex. Walk `items(..., key = ...)` / `itemsIndexed(..., key = ...)` hits and read the surrounding context:
|
|
110
|
+
|
|
111
|
+
- is the list source a merge / combine / concatenation of multiple flows?
|
|
112
|
+
- does the backend spec guarantee ID uniqueness?
|
|
113
|
+
- is the key computed from `hashCode()` on a non-`data class`?
|
|
114
|
+
|
|
115
|
+
When uniqueness is not guaranteed, flag as a latent crash and suggest a dedup index or a synthesized key like `"${source}-${id}"`.
|
|
116
|
+
|
|
117
|
+
### Scaffold Inner-Padding Heuristic
|
|
118
|
+
|
|
119
|
+
`Scaffold` exposes `innerPadding` to its content lambda. If the content ignores it, elements are drawn behind the `TopAppBar` or `BottomAppBar`. Search for `Scaffold(` and read each hit — the content lambda parameter should be applied to the root-most child via `Modifier.padding(innerPadding)` (or `.consumeWindowInsets(innerPadding)`). If a `Scaffold { }` discards the padding parameter with `_ ->` or omits it entirely while nesting non-trivial content, flag it.
|
|
120
|
+
|
|
121
|
+
### Strong Skipping Mode Check
|
|
122
|
+
|
|
123
|
+
Confirm the project's compiler version:
|
|
124
|
+
|
|
125
|
+
- `rg -n 'kotlin\s*=\s*"' -g '*.toml'` and `rg -n 'org\.jetbrains\.kotlin' -g '*.gradle*'` to find the Kotlin version
|
|
126
|
+
- Strong Skipping is on by default at Kotlin **2.0.20+**; below that, stability inference matters more and unstable params more aggressively block skipping
|
|
127
|
+
|
|
128
|
+
## 4. State Management Checks
|
|
129
|
+
|
|
130
|
+
### Search For
|
|
131
|
+
|
|
132
|
+
- `mutableStateOf`
|
|
133
|
+
- `mutableIntStateOf|mutableLongStateOf|mutableFloatStateOf|mutableDoubleStateOf`
|
|
134
|
+
- `mutableStateListOf|mutableStateMapOf`
|
|
135
|
+
- `rememberSaveable`
|
|
136
|
+
- `mapSaver|listSaver|@Parcelize|Saver`
|
|
137
|
+
- `collectAsStateWithLifecycle`
|
|
138
|
+
- `collectAsState`
|
|
139
|
+
- `observeAsState`
|
|
140
|
+
- `subscribeAsState`
|
|
141
|
+
- `MutableState<`
|
|
142
|
+
- `State<`
|
|
143
|
+
- `mutableListOf|mutableMapOf|mutableSetOf|ArrayList`
|
|
144
|
+
- `CompositionLocal`
|
|
145
|
+
- `compositionLocalOf|staticCompositionLocalOf`
|
|
146
|
+
- `ViewModel`
|
|
147
|
+
- `viewModel\(` — log invocation depth (screen entry vs. deep tree)
|
|
148
|
+
- `mutableStateOf` / `mutableIntStateOf` etc. declared as members of a class extending `ViewModel` (not inside a composable)
|
|
149
|
+
- `Channel<` / `receiveAsFlow\(\)` / `consumeAsFlow\(\)` exposed from a `ViewModel` for UI events
|
|
150
|
+
- `\.stateIn\s*\(` — positive signal; check for `WhileSubscribed(5_000)` or similar timeout
|
|
151
|
+
- `rememberSaveable` invoked inside a `Lazy(Column|Row|VerticalGrid|HorizontalGrid|VerticalStaggeredGrid|HorizontalStaggeredGrid)` item factory
|
|
152
|
+
|
|
153
|
+
### Red Flags To Verify
|
|
154
|
+
|
|
155
|
+
- duplicated state across parent/child or sibling composables
|
|
156
|
+
- shared state held too low in the tree
|
|
157
|
+
- reusable components with unnecessary internal state
|
|
158
|
+
- Android UI code using `collectAsState()` where `collectAsStateWithLifecycle()` is a better fit (skip this rule on Compose Multiplatform code paths)
|
|
159
|
+
- non-observable mutable collections used as state (`mutableListOf` mutated in place)
|
|
160
|
+
- `mutableListOf` / `mutableMapOf` wrapped in a `MutableState` instead of `mutableStateListOf` / `mutableStateMapOf` — element changes won't trigger recomposition
|
|
161
|
+
- `mutableStateOf<Int|Long|Float|Double>` instead of the typed factory (autoboxing)
|
|
162
|
+
- `MutableState<T>` params in reusable components
|
|
163
|
+
- `State<T>` params where a value or lambda would be more flexible
|
|
164
|
+
- `remember { ... }` with no `key` for a value that depends on inputs (stale cache)
|
|
165
|
+
- `rememberSaveable { mutableStateOf(SomeNonBundleable(...)) }` without a `Saver` — restoration silently fails
|
|
166
|
+
|
|
167
|
+
### Positive Signals
|
|
168
|
+
|
|
169
|
+
- clear stateful wrapper + stateless reusable composable split
|
|
170
|
+
- state hoisted to the lowest common reader / highest writer
|
|
171
|
+
- related state hoisted together
|
|
172
|
+
- lifecycle-aware collection of flows in Android UI
|
|
173
|
+
- plain state-holder classes for larger screens or app shells
|
|
174
|
+
|
|
175
|
+
## 5. Side Effects Checks
|
|
176
|
+
|
|
177
|
+
### Search For
|
|
178
|
+
|
|
179
|
+
- `LaunchedEffect`
|
|
180
|
+
- `DisposableEffect`
|
|
181
|
+
- `SideEffect`
|
|
182
|
+
- `rememberUpdatedState`
|
|
183
|
+
- `produceState`
|
|
184
|
+
- `snapshotFlow`
|
|
185
|
+
- `rememberCoroutineScope`
|
|
186
|
+
- `\.launch\s*[\({]` — catches both `scope.launch {` and `scope.launch(Dispatchers.IO) {`
|
|
187
|
+
- `Thread\(`
|
|
188
|
+
- `GlobalScope`
|
|
189
|
+
- `LifecycleEventObserver`
|
|
190
|
+
- `BackHandler`
|
|
191
|
+
- `NavHost`, `composable\(` (in nav graphs), `navController\.navigate`
|
|
192
|
+
- string-based nav routes: `composable\(\s*"` and `navigate\(\s*"` (suggest type-safe `@Serializable` routes on Navigation Compose 2.8+)
|
|
193
|
+
|
|
194
|
+
### Red Flags To Verify
|
|
195
|
+
|
|
196
|
+
- work started directly in composition body
|
|
197
|
+
- navigation, snackbar, analytics, or repository calls triggered during composition instead of from an effect or event path
|
|
198
|
+
- `navController.navigate(...)` invoked in composition body — must come from an event handler or `LaunchedEffect`
|
|
199
|
+
- `LaunchedEffect(Unit)` / `LaunchedEffect(true)` is **not** suspicious on its own (the "run once" pattern is idiomatic). Only flag it when the body captures parameter or state values that may change without `rememberUpdatedState`
|
|
200
|
+
- `DisposableEffect` with empty or suspicious cleanup
|
|
201
|
+
- listener/observer registration without `onDispose`
|
|
202
|
+
- effect keys too broad or too narrow
|
|
203
|
+
- `rememberCoroutineScope()` used to launch keyed/long-lived work that belongs in a `LaunchedEffect`
|
|
204
|
+
- `snapshotFlow { ... }` invoked outside an effect, or used to compute a value that `derivedStateOf` would handle more cheaply
|
|
205
|
+
- `derivedStateOf { a + b }`-style misuse — when input frequency ≈ output frequency it is pure overhead (the official antipattern)
|
|
206
|
+
|
|
207
|
+
### Positive Signals
|
|
208
|
+
|
|
209
|
+
- `rememberUpdatedState` used for long-lived effects that should keep latest callbacks
|
|
210
|
+
- `DisposableEffect` paired with clear cleanup
|
|
211
|
+
- `SideEffect` used only to publish state to non-Compose code after successful composition
|
|
212
|
+
- `produceState` or equivalent used for converting external async sources into Compose state
|
|
213
|
+
- `snapshotFlow { ... }` collected from inside a `LaunchedEffect` for Compose-state → Flow conversions
|
|
214
|
+
- `rememberCoroutineScope()` used only for event-driven work (button taps, gesture handlers)
|
|
215
|
+
|
|
216
|
+
## 6. Composable API Quality Checks
|
|
217
|
+
|
|
218
|
+
Focus on shared components and internal UI kit code, not every screen.
|
|
219
|
+
|
|
220
|
+
### Search For
|
|
221
|
+
|
|
222
|
+
- shared component directories such as `components`, `commonui`, `designsystem`, `ui/components`
|
|
223
|
+
- function signatures around `@Composable`
|
|
224
|
+
- `modifier: Modifier =`
|
|
225
|
+
- `Modifier = Modifier\.` — non-no-op modifier defaults
|
|
226
|
+
- `MutableState<`
|
|
227
|
+
- `State<`
|
|
228
|
+
- `CompositionLocal`
|
|
229
|
+
- `compositionLocalOf|staticCompositionLocalOf` — definitions
|
|
230
|
+
- `CompositionLocalProvider` — provision sites
|
|
231
|
+
- ViewModels in CompositionLocal: `compositionLocalOf<.*ViewModel`, `staticCompositionLocalOf<.*ViewModel`
|
|
232
|
+
- `viewModel\(` — invocation sites; flag when called below the screen entry composable
|
|
233
|
+
- slot APIs and receiver scopes: `RowScope\.`, `ColumnScope\.`, `BoxScope\.`, `content:\s*@Composable`
|
|
234
|
+
- modifier authoring: `Modifier\.composed\s*\{` (discouraged), `Modifier\.Node`, `ModifierNodeElement`
|
|
235
|
+
- movable content: `movableContentOf`, `movableContentWithReceiverOf`
|
|
236
|
+
- variant smells: `\bstyle:\s*\w+Style\b` — single-component-with-style-enum
|
|
237
|
+
- `Basic` prefix: `fun Basic[A-Z]\w+\s*\(`
|
|
238
|
+
- lazy list `contentType`: `contentType\s*=` inside `items(` / `itemsIndexed(` calls — its presence on heterogeneous lists is a positive signal; its absence on heterogeneous lists is a deduction
|
|
239
|
+
|
|
240
|
+
### Red Flags To Verify
|
|
241
|
+
|
|
242
|
+
- shared components missing a `modifier`
|
|
243
|
+
- `modifier` not being the first optional parameter
|
|
244
|
+
- `modifier` default not equal to `Modifier` (e.g. `modifier: Modifier = Modifier.padding(8.dp)` — caller's modifier silently loses the padding)
|
|
245
|
+
- multiple modifier parameters on one component
|
|
246
|
+
- modifier applied to a child instead of the root-most emitted UI, and not as the *first* link in the chain
|
|
247
|
+
- nullable params used to mean "choose internal default" (expose a `ComponentDefaults` object instead)
|
|
248
|
+
- component-specific configuration hidden behind implicit locals
|
|
249
|
+
- huge multipurpose components that should be layered or split
|
|
250
|
+
- behavior added as parameters that should be modifiers (`onClick`, `clipToCircle`)
|
|
251
|
+
- a single component with a `style: ButtonStyle` enum-like parameter instead of distinct `ContainedButton` / `OutlinedButton` / `TextButton` components
|
|
252
|
+
- custom modifiers built with `Modifier.composed { ... }` (discouraged in favor of `Modifier.Node`)
|
|
253
|
+
- `MutableState<T>` params — replace with `value: T` (immediate read) or `value: () -> T` (deferred) plus `onValueChange: (T) -> Unit`
|
|
254
|
+
- `@Composable` UI-emitting functions named in lowerCamelCase or returning a non-Unit value (style guide violation)
|
|
255
|
+
|
|
256
|
+
### Positive Signals
|
|
257
|
+
|
|
258
|
+
- required params first, then `modifier`, then optional params, then trailing content lambda
|
|
259
|
+
- explicit config exposed as parameters with meaningful defaults via a `ComponentDefaults` object
|
|
260
|
+
- focused component responsibilities
|
|
261
|
+
- internal wrappers built on simpler lower-level components
|
|
262
|
+
- slot lambdas (`content: @Composable RowScope.() -> Unit`) for flexible composition
|
|
263
|
+
- `Basic*` variants alongside opinionated public versions
|
|
264
|
+
- distinct components per visual variant (no `style` enum)
|
|
265
|
+
- `movableContentOf` used to preserve slot lifecycle across structural moves
|
|
266
|
+
- custom modifiers authored with `Modifier.Node` / `ModifierNodeElement`
|
|
267
|
+
|
|
268
|
+
### Modifier-Order Smell
|
|
269
|
+
|
|
270
|
+
No clean regex. When reading shared components, watch for `Modifier.padding(...).clickable {}` vs `Modifier.clickable {}.padding(...)` — they produce different ripple bounds and hit areas. Flag when the choice looks accidental in a reusable component (the wrong order is almost always a bug there; in a one-off screen it may be intentional).
|
|
271
|
+
|
|
272
|
+
## 7. Read Representative Files
|
|
273
|
+
|
|
274
|
+
For each category, read enough code to cover:
|
|
275
|
+
|
|
276
|
+
- at least one screen
|
|
277
|
+
- at least one shared component area
|
|
278
|
+
- at least one state-owning area such as a ViewModel or state-holder
|
|
279
|
+
- any suspicious files surfaced by search
|
|
280
|
+
|
|
281
|
+
Do not rely on one "bad" file to characterize the entire repo.
|
|
282
|
+
|
|
283
|
+
## 8. Merge Repeated Findings
|
|
284
|
+
|
|
285
|
+
Prefer:
|
|
286
|
+
|
|
287
|
+
- "7 shared components miss `modifier`"
|
|
288
|
+
|
|
289
|
+
over:
|
|
290
|
+
|
|
291
|
+
- seven separate bullets saying the same thing
|
|
292
|
+
|
|
293
|
+
Still keep 2-4 concrete file examples to justify the systemic finding.
|
|
294
|
+
|
|
295
|
+
## 9. Out-Of-Scope Cases
|
|
296
|
+
|
|
297
|
+
Stop or narrow scope if:
|
|
298
|
+
|
|
299
|
+
- the repo is not Android Jetpack Compose
|
|
300
|
+
- Compose is only present in demo or sample code
|
|
301
|
+
- the user asked to audit only one module or feature
|
|
302
|
+
|
|
303
|
+
In those cases, explain the limitation and score only the relevant surface area.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Gradle init script injected by the Jetpack Compose audit skill.
|
|
2
|
+
//
|
|
3
|
+
// Purpose: enable Compose Compiler reports + metrics for every module that
|
|
4
|
+
// applies the Compose Compiler plugin, without modifying the user's build
|
|
5
|
+
// files. Each module's output lands in <module>/build/compose_audit/.
|
|
6
|
+
//
|
|
7
|
+
// Usage (from the skill, not the user):
|
|
8
|
+
// ./gradlew <task> --init-script <path-to-this-file> --no-daemon
|
|
9
|
+
//
|
|
10
|
+
// Two code paths:
|
|
11
|
+
// A. Modern Compose Compiler Gradle plugin (Kotlin 2.0+) — configures the
|
|
12
|
+
// `composeCompiler { }` extension directly.
|
|
13
|
+
// B. Legacy compiler-plugin argument fallback — injects reportsDestination
|
|
14
|
+
// and metricsDestination via Kotlin compile task free args. Works on
|
|
15
|
+
// older toolchains that still apply the compiler plugin manually.
|
|
16
|
+
//
|
|
17
|
+
// The script is defensive: failures in one module must not fail the build.
|
|
18
|
+
|
|
19
|
+
allprojects { project ->
|
|
20
|
+
project.afterEvaluate {
|
|
21
|
+
def reportDir = new File(project.buildDir, "compose_audit")
|
|
22
|
+
|
|
23
|
+
// Path A: modern Compose Compiler Gradle plugin.
|
|
24
|
+
def composeExt = project.extensions.findByName("composeCompiler")
|
|
25
|
+
if (composeExt != null) {
|
|
26
|
+
try {
|
|
27
|
+
reportDir.mkdirs()
|
|
28
|
+
composeExt.reportsDestination.set(reportDir)
|
|
29
|
+
composeExt.metricsDestination.set(reportDir)
|
|
30
|
+
project.logger.lifecycle("compose-audit: reports -> ${reportDir}")
|
|
31
|
+
return
|
|
32
|
+
} catch (Throwable e) {
|
|
33
|
+
project.logger.warn("compose-audit: composeCompiler extension found but could not be configured: ${e.message}")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Path B: legacy compiler-args fallback for projects that apply the
|
|
38
|
+
// Compose compiler plugin the old way (pre-Kotlin-2.0).
|
|
39
|
+
def anyConfigured = false
|
|
40
|
+
project.tasks.matching { it.name.startsWith("compile") && it.name.contains("Kotlin") }.configureEach { task ->
|
|
41
|
+
try {
|
|
42
|
+
def opts = task.hasProperty("compilerOptions") ? task.compilerOptions : null
|
|
43
|
+
if (opts == null) return
|
|
44
|
+
opts.freeCompilerArgs.addAll([
|
|
45
|
+
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${reportDir.absolutePath}",
|
|
46
|
+
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${reportDir.absolutePath}",
|
|
47
|
+
])
|
|
48
|
+
reportDir.mkdirs()
|
|
49
|
+
anyConfigured = true
|
|
50
|
+
} catch (Throwable ignored) {
|
|
51
|
+
// Silent — task may not be a Kotlin compile task.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (anyConfigured) {
|
|
55
|
+
project.logger.lifecycle("compose-audit: reports -> ${reportDir} (legacy compiler-args path)")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-compose-state
|
|
3
|
+
description: Jetpack Compose 状态管理与副作用的深度指引。覆盖 StateFlow/SharedFlow 收集、LaunchedEffect/DisposableEffect/SideEffect 选用、rememberUpdatedState 防过期、produceState 桥接非 Compose API。刚性约束由 rules 加载,本技能聚焦正确选用和常见陷阱。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Compose 状态与副作用 — 实现指引
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
状态管理是 Compose 开发中最容易出 bug 的领域。本技能聚焦如何正确选用状态 API 和副作用 API,以及常见陷阱。
|
|
11
|
+
|
|
12
|
+
> **刚性约束已由 rules 加载,本技能不再重复。** 详见:
|
|
13
|
+
> - `harness-compose-mandatory` — 无状态 Composable、副作用约束
|
|
14
|
+
> - `harness-coroutines-scope` — collectAsStateWithLifecycle、SharedFlow replay 约束
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 副作用 API 选用指南
|
|
19
|
+
|
|
20
|
+
| API | 场景 | 特点 |
|
|
21
|
+
|-----|------|------|
|
|
22
|
+
| `LaunchedEffect(key)` | 需要协程的副作用(网络、数据库) | key 变化时取消旧协程、启动新协程 |
|
|
23
|
+
| `DisposableEffect(key)` | 需要清理的绑定(注册/注销监听器) | 提供 `onDispose` 清理块 |
|
|
24
|
+
| `SideEffect {}` | 每次成功重组后执行的非挂起操作 | 不常用,通常 `LaunchedEffect(Unit)` 更好 |
|
|
25
|
+
| `rememberUpdatedState` | 防止 LaunchedEffect 内 lambda 过期 | 不重启 Effect,只更新引用 |
|
|
26
|
+
| `produceState` | 将非 Compose API 桥接为 State | 内部启动协程,emit 到 State |
|
|
27
|
+
| `derivedStateOf` | 派生计算型状态 | 只在结果变化时触发重组 |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## LaunchedEffect — 协程副作用
|
|
32
|
+
|
|
33
|
+
```kotlin
|
|
34
|
+
// 根据 key 重启
|
|
35
|
+
@Composable
|
|
36
|
+
fun UserProfile(userId: String, viewModel: UserViewModel) {
|
|
37
|
+
LaunchedEffect(userId) {
|
|
38
|
+
viewModel.loadUser(userId) // userId 变化时自动取消旧的、启动新的
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// "运行一次"模式(惯用写法,不需要 rememberUpdatedState 包裹)
|
|
43
|
+
@Composable
|
|
44
|
+
fun DataLoader(viewModel: MyViewModel) {
|
|
45
|
+
LaunchedEffect(Unit) {
|
|
46
|
+
viewModel.loadInitialData()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## DisposableEffect — 需要清理的绑定
|
|
52
|
+
|
|
53
|
+
```kotlin
|
|
54
|
+
@Composable
|
|
55
|
+
fun LifecycleAwareComponent(lifecycle: Lifecycle) {
|
|
56
|
+
DisposableEffect(lifecycle) {
|
|
57
|
+
val observer = LifecycleEventObserver { _, event -> /* 处理 */ }
|
|
58
|
+
lifecycle.addObserver(observer)
|
|
59
|
+
onDispose { lifecycle.removeObserver(observer) }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## rememberUpdatedState — 防过期
|
|
65
|
+
|
|
66
|
+
```kotlin
|
|
67
|
+
// ❌ 危险:LaunchedEffect 内捕获的 value 可能过期
|
|
68
|
+
@Composable
|
|
69
|
+
fun SearchScreen(onTimeout: () -> Unit) {
|
|
70
|
+
LaunchedEffect(Unit) {
|
|
71
|
+
delay(5000)
|
|
72
|
+
onTimeout() // 5 秒后调用的是启动时的旧 lambda
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ 安全:rememberUpdatedState 保持最新引用
|
|
77
|
+
@Composable
|
|
78
|
+
fun SearchScreen(onTimeout: () -> Unit) {
|
|
79
|
+
val currentOnTimeout by rememberUpdatedState(onTimeout)
|
|
80
|
+
LaunchedEffect(Unit) {
|
|
81
|
+
delay(5000)
|
|
82
|
+
currentOnTimeout() // 始终调用最新的 lambda
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## produceState — 桥接非 Compose API
|
|
88
|
+
|
|
89
|
+
```kotlin
|
|
90
|
+
@Composable
|
|
91
|
+
fun loadUser(userId: String): State<Result<User>> {
|
|
92
|
+
return produceState(initialValue = Result.loading(), key1 = userId) {
|
|
93
|
+
val result = userRepository.getUser(userId)
|
|
94
|
+
value = result
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## StateFlow 与 SharedFlow 在 Compose 中的使用
|
|
102
|
+
|
|
103
|
+
### 持久状态 → StateFlow
|
|
104
|
+
|
|
105
|
+
```kotlin
|
|
106
|
+
// ViewModel 中
|
|
107
|
+
private val _uiState = MutableStateFlow(UiState())
|
|
108
|
+
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
|
109
|
+
|
|
110
|
+
// Compose 中收集
|
|
111
|
+
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 一次性事件 → SharedFlow
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
// ViewModel 中
|
|
118
|
+
private val _effects = MutableSharedFlow<Effect>(replay = 0) // replay=0 是强制约束
|
|
119
|
+
val effects: SharedFlow<Effect> = _effects.asSharedFlow()
|
|
120
|
+
|
|
121
|
+
// Compose 中收集
|
|
122
|
+
LaunchedEffect(Unit) {
|
|
123
|
+
viewModel.effects.collect { effect ->
|
|
124
|
+
when (effect) {
|
|
125
|
+
is Effect.ShowToast -> { /* 显示 Toast */ }
|
|
126
|
+
is Effect.Navigate -> { /* 导航 */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 从 Flow 派生 StateFlow
|
|
133
|
+
|
|
134
|
+
```kotlin
|
|
135
|
+
val searchResults: StateFlow<List<Item>> = _searchQuery
|
|
136
|
+
.debounce(300)
|
|
137
|
+
.filter { it.length >= 2 }
|
|
138
|
+
.flatMapLatest { query -> searchRepository.search(query) }
|
|
139
|
+
.stateIn(
|
|
140
|
+
scope = viewModelScope,
|
|
141
|
+
started = SharingStarted.WhileSubscribed(5000),
|
|
142
|
+
initialValue = emptyList()
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 常见陷阱
|
|
149
|
+
|
|
150
|
+
### 1. collectAsState vs collectAsStateWithLifecycle
|
|
151
|
+
|
|
152
|
+
```kotlin
|
|
153
|
+
// ❌ 不感知生命周期,后台仍在收集
|
|
154
|
+
val state by viewModel.uiState.collectAsState()
|
|
155
|
+
|
|
156
|
+
// ✅ 感知生命周期,后台暂停收集
|
|
157
|
+
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. SharedFlow replay > 0
|
|
161
|
+
|
|
162
|
+
```kotlin
|
|
163
|
+
// ❌ 新订阅者会收到旧事件
|
|
164
|
+
MutableSharedFlow<Effect>(replay = 1)
|
|
165
|
+
|
|
166
|
+
// ✅ 一次性消费
|
|
167
|
+
MutableSharedFlow<Effect>(replay = 0)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. 在 Reducer 中执行副作用
|
|
171
|
+
|
|
172
|
+
```kotlin
|
|
173
|
+
// ❌ Reducer 内触发导航
|
|
174
|
+
is Event.Saved -> {
|
|
175
|
+
navigator.navigate(Home) // 副作用!
|
|
176
|
+
state.copy(isSaved = true)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ✅ 副作用放 EffectHandler
|
|
180
|
+
override val effectHandler = EffectHandler { event, emit ->
|
|
181
|
+
when (event) {
|
|
182
|
+
is Event.Saved -> emit(Effect.NavigateHome)
|
|
183
|
+
else -> {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 必须遵守的约束规则
|
|
191
|
+
|
|
192
|
+
> rules 引用路径:`../rules/<rule-name>.mdc`
|
|
193
|
+
|
|
194
|
+
- harness-compose-mandatory
|
|
195
|
+
- harness-coroutines-scope
|
|
196
|
+
- harness-mvi-layering
|