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.
Files changed (134) hide show
  1. package/README.md +531 -0
  2. package/bin/cli.js +3 -0
  3. package/dist/adapters/_frontmatter.js +24 -0
  4. package/dist/adapters/claude-code.js +12 -0
  5. package/dist/adapters/codechat.js +12 -0
  6. package/dist/adapters/cursor.js +19 -0
  7. package/dist/adapters/generic.js +19 -0
  8. package/dist/adapters/index.js +26 -0
  9. package/dist/adapters/qoder.js +12 -0
  10. package/dist/commands/apply.js +272 -0
  11. package/dist/commands/init.js +420 -0
  12. package/dist/core/agent-injector.js +192 -0
  13. package/dist/core/next-steps.js +91 -0
  14. package/dist/core/render-meta.js +81 -0
  15. package/dist/core/repomix-pack.js +54 -0
  16. package/dist/core/scaffold.js +93 -0
  17. package/dist/core/state.js +80 -0
  18. package/dist/index.js +239 -0
  19. package/dist/types.js +5 -0
  20. package/dist/utils/baseline-copy.js +591 -0
  21. package/dist/utils/baseline-defaults.js +106 -0
  22. package/dist/utils/logger.js +56 -0
  23. package/dist/utils/validate-args.js +132 -0
  24. package/dist/utils/version.js +23 -0
  25. package/dist/wiki/abort.js +30 -0
  26. package/dist/wiki/config.js +79 -0
  27. package/dist/wiki/defaults.js +16 -0
  28. package/dist/wiki/envLoader.js +78 -0
  29. package/dist/wiki/index.js +29 -0
  30. package/dist/wiki/openaiCompat.js +219 -0
  31. package/dist/wiki/repowikiCanonicalSections.js +67 -0
  32. package/dist/wiki/repowikiCheckpoint.js +106 -0
  33. package/dist/wiki/repowikiConfig.js +9 -0
  34. package/dist/wiki/repowikiGit.js +73 -0
  35. package/dist/wiki/repowikiIndexer.js +824 -0
  36. package/dist/wiki/repowikiMarkdownPost.js +123 -0
  37. package/dist/wiki/repowikiMetadataContent.js +64 -0
  38. package/dist/wiki/repowikiMetadataJson.js +15 -0
  39. package/dist/wiki/repowikiScanner.js +156 -0
  40. package/dist/wiki/repowikiStructureNav.js +286 -0
  41. package/dist/wiki/repowikiStructureNormalize.js +218 -0
  42. package/dist/wiki/wikiStructureXml.js +316 -0
  43. package/dist/wiki/wikiTasksWriter.js +127 -0
  44. package/package.json +57 -0
  45. package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
  46. package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
  47. package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
  48. package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
  49. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
  50. package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
  51. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
  52. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
  53. package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
  54. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
  55. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
  56. package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
  57. package/templates/_shared/meta/AGENTS.md.ejs +53 -0
  58. package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
  59. package/templates/_shared/meta/README.md.ejs +51 -0
  60. package/templates/_shared/meta/VERSION.ejs +1 -0
  61. package/templates/_shared/meta/harness.yaml.ejs +52 -0
  62. package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
  63. package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
  64. package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
  65. package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
  66. package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
  67. package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
  68. package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
  69. package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
  70. package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
  71. package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
  72. package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
  73. package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
  74. package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
  75. package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
  76. package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
  77. package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
  78. package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
  79. package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
  80. package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
  81. package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
  82. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
  83. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
  84. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
  85. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
  86. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
  87. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
  88. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
  89. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
  90. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
  91. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
  92. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
  93. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
  94. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
  95. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
  96. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
  97. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
  98. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
  99. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
  100. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
  101. package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
  102. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
  103. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
  104. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
  105. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
  106. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
  107. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
  108. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
  109. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
  110. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
  111. 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
  112. 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
  113. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
  114. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
  115. package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
  116. package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
  117. package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
  118. package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
  119. package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
  120. package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
  121. package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
  122. package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
  123. package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
  124. package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
  125. package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
  126. package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
  127. package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
  128. package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
  129. package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
  130. package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
  131. package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
  132. package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
  133. package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
  134. 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