@michaelbel/cuckcoder-mcp 1.6.11 → 1.6.12

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 (38) hide show
  1. package/assets/rules/app-badging.md +170 -0
  2. package/assets/rules/architecture.md +3 -3
  3. package/assets/rules/{bottom-sheet.md → compose-bottom-sheet.md} +3 -3
  4. package/assets/rules/compose-color.md +3 -3
  5. package/assets/rules/compose-constraintlayout.md +3 -3
  6. package/assets/rules/{dialog.md → compose-dialog.md} +3 -3
  7. package/assets/rules/{lazylist.md → compose-lazy.md} +3 -5
  8. package/assets/rules/{shimmer.md → compose-loading.md} +3 -5
  9. package/assets/rules/{preview.md → compose-preview.md} +6 -5
  10. package/assets/rules/{scaffold.md → compose-scaffold.md} +3 -5
  11. package/assets/rules/compose-screen.md +3 -3
  12. package/assets/rules/compose-spacing.md +3 -3
  13. package/assets/rules/{textfield.md → compose-textfield.md} +1 -5
  14. package/assets/rules/{typography.md → compose-typography.md} +3 -5
  15. package/assets/rules/compose.md +13 -3
  16. package/assets/rules/domain.md +3 -3
  17. package/assets/rules/filesystem.md +3 -1
  18. package/assets/rules/git.md +5 -1
  19. package/assets/rules/github-readme.md +5 -1
  20. package/assets/rules/github-repo.md +5 -1
  21. package/assets/rules/{navigation.md → jetpack-navigation.md} +3 -5
  22. package/assets/rules/{room.md → jetpack-room.md} +7 -5
  23. package/assets/rules/{workmanager.md → jetpack-work.md} +3 -5
  24. package/assets/rules/kmp.md +3 -3
  25. package/assets/rules/kotlin-coroutines.md +15 -0
  26. package/assets/rules/kotlin.md +5 -4
  27. package/assets/rules/mvi-error-handling.md +3 -5
  28. package/assets/rules/mvi-state.md +3 -5
  29. package/assets/rules/mvi.md +5 -5
  30. package/assets/rules/network.md +3 -5
  31. package/assets/rules/realtime.md +3 -5
  32. package/assets/rules/resource.md +4 -5
  33. package/assets/rules/security.md +112 -0
  34. package/assets/rules/usecase.md +3 -5
  35. package/assets/rules/workflow.md +5 -1
  36. package/assets/skills/create-feature-bottom-sheet/SKILL.md +31 -26
  37. package/package.json +1 -1
  38. package/dist/github.js +0 -23
@@ -0,0 +1,170 @@
1
+ ---
2
+ description: >-
3
+ Регресс-гард доступности устройств: aapt2 dump badging в golden-файл app/badging, обязательный
4
+ check<Variant>Badging в CI, необязательное железо через android:required=false
5
+ paths:
6
+ - "**/*.gradle.kts"
7
+ - "**/AndroidManifest.xml"
8
+ ---
9
+
10
+ `aapt2 dump badging` по собранному APK показывает эффективный набор `uses-feature` /
11
+ `uses-permission` / `uses-sdk` — то, из-за чего Google Play скрывает приложение на планшетах,
12
+ Android TV, Wear OS, складных устройствах и авто. Обновление зависимости может молча добавить
13
+ `android:required` hardware feature (камера, телефония, автофокус) и срезать доступность, а в
14
+ диффе исходников этого не видно. Гард фиксирует бейдж в golden-файл и валит CI при расхождении.
15
+
16
+ ## Что заводится в проекте
17
+
18
+ - Convention-плагин `build-logic/`, который на каждый application-вариант регистрирует три задачи:
19
+ - `generate<Variant>Badging` — `aapt2 dump badging` по универсальному APK варианта → генерируемый
20
+ файл в `build/`.
21
+ - `update<Variant>Badging` — копирует генерируемый файл в golden `app/badging/<variant>.txt`
22
+ (запускается локально, результат коммитится и проходит ревью).
23
+ - `check<Variant>Badging` — сравнивает генерируемый с golden; при расхождении фейлит с диффом.
24
+ - Golden-файлы `app/badging/*.txt` — под контролем версий; изменение бейджа всегда осознанное и
25
+ видно в PR.
26
+
27
+ ## Правила
28
+
29
+ - `check<Variant>Badging` (как минимум для release-варианта) — обязательный шаг CI на каждый PR.
30
+ - Изменение `app/badging/*.txt` в PR без соответствующей причины (новая фича, осознанное
31
+ требование к железу) — стоп-фактор ревью.
32
+ - Все `uses-feature` для необязательного железа объявляй с `android:required="false"`; жёсткое
33
+ требование железа — только когда приложение без него нефункционально.
34
+ - Не добавляй `check*Badging` в `dependsOn` обычной сборки — это отдельный verification-шаг, чтобы
35
+ не замедлять локальный цикл.
36
+
37
+ ## Скелет convention-плагина
38
+
39
+ ```kotlin
40
+ // build-logic/convention/src/main/kotlin/AndroidApplicationBadgingConventionPlugin.kt
41
+ import com.android.build.api.artifact.SingleArtifact
42
+ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
43
+ import org.gradle.api.DefaultTask
44
+ import org.gradle.api.Plugin
45
+ import org.gradle.api.Project
46
+ import org.gradle.api.file.RegularFileProperty
47
+ import org.gradle.api.provider.Property
48
+ import org.gradle.api.tasks.CacheableTask
49
+ import org.gradle.api.tasks.Copy
50
+ import org.gradle.api.tasks.InputFile
51
+ import org.gradle.api.tasks.OutputFile
52
+ import org.gradle.api.tasks.PathSensitive
53
+ import org.gradle.api.tasks.PathSensitivity
54
+ import org.gradle.api.tasks.TaskAction
55
+ import org.gradle.kotlin.dsl.register
56
+
57
+ class AndroidApplicationBadgingConventionPlugin : Plugin<Project> {
58
+ override fun apply(target: Project) {
59
+ val components = target.extensions
60
+ .getByType(ApplicationAndroidComponentsExtension::class.java)
61
+
62
+ components.onVariants { variant ->
63
+ val capitalised = variant.name.replaceFirstChar { it.uppercaseChar() }
64
+ val golden = target.rootDir.resolve("app/badging/${variant.name}.txt")
65
+ val generated = target.layout.buildDirectory.file("badging/${variant.name}.txt")
66
+ val apk = variant.artifacts.get(SingleArtifact.APK)
67
+
68
+ val generate = target.tasks.register<GenerateBadgingTask>("generate${capitalised}Badging") {
69
+ apkDir.set(apk)
70
+ aapt2.set(resolveAapt2(target))
71
+ output.set(generated)
72
+ }
73
+
74
+ target.tasks.register<Copy>("update${capitalised}Badging") {
75
+ group = "badging"
76
+ description = "Обновляет app/badging/${variant.name}.txt — закоммить результат"
77
+ dependsOn(generate)
78
+ from(generated)
79
+ into(golden.parentFile.also { it.mkdirs() })
80
+ rename { golden.name }
81
+ }
82
+
83
+ target.tasks.register<CheckBadgingTask>("check${capitalised}Badging") {
84
+ group = "verification"
85
+ description = "Фейлит, если требуемые фичи ${variant.name}-APK разошлись с golden-файлом"
86
+ dependsOn(generate)
87
+ generated.set(generated)
88
+ this.golden.set(golden)
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ @CacheableTask
95
+ abstract class GenerateBadgingTask : DefaultTask() {
96
+ @get:InputFile @get:PathSensitive(PathSensitivity.NONE)
97
+ abstract val apkDir: RegularFileProperty
98
+
99
+ @get:org.gradle.api.tasks.Input
100
+ abstract val aapt2: Property<String>
101
+
102
+ @get:OutputFile
103
+ abstract val output: RegularFileProperty
104
+
105
+ @TaskAction
106
+ fun run() {
107
+ val apk = apkDir.get().asFile.parentFile
108
+ .listFiles { f -> f.extension == "apk" }?.firstOrNull()
109
+ ?: error("APK не найден в ${apkDir.get().asFile.parentFile}")
110
+ val out = output.get().asFile.also { it.parentFile.mkdirs() }
111
+ project.exec {
112
+ commandLine(aapt2.get(), "dump", "badging", apk.absolutePath)
113
+ standardOutput = out.outputStream()
114
+ }
115
+ }
116
+ }
117
+
118
+ @CacheableTask
119
+ abstract class CheckBadgingTask : DefaultTask() {
120
+ @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
121
+ abstract val generated: RegularFileProperty
122
+
123
+ @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
124
+ abstract val golden: RegularFileProperty
125
+
126
+ @TaskAction
127
+ fun run() {
128
+ val g = generated.get().asFile.readText()
129
+ val expected = golden.get().asFile.let {
130
+ if (!it.exists()) error("Нет golden-файла ${it.path} — запусти update…Badging и закоммить")
131
+ it.readText()
132
+ }
133
+ if (g.trim() != expected.trim()) {
134
+ error(
135
+ "Бейдж APK разошёлся с golden-файлом ${golden.get().asFile.path}.\n" +
136
+ "Если изменение осознанное — запусти ./gradlew update…Badging и закоммить новый golden."
137
+ )
138
+ }
139
+ }
140
+ }
141
+
142
+ private fun resolveAapt2(project: Project): String {
143
+ val android = project.extensions
144
+ .getByType(com.android.build.gradle.BaseExtension::class.java)
145
+ return android.sdkDirectory
146
+ .resolve("build-tools/${android.buildToolsVersion}/aapt2")
147
+ .absolutePath
148
+ }
149
+ ```
150
+
151
+ ## CI
152
+
153
+ ```yaml
154
+ # .github/workflows/pr-check.yml (фрагмент)
155
+ - name: App badging check
156
+ run: ./gradlew checkReleaseBadging
157
+ ```
158
+
159
+ ## Как выглядит golden-файл
160
+
161
+ ```
162
+ # app/badging/release.txt
163
+ package: name='com.example.app' versionCode='142' versionName='1.4.2'
164
+ sdkVersion:'26'
165
+ targetSdkVersion:'35'
166
+ uses-permission: name='android.permission.INTERNET'
167
+ uses-feature-not-required: name='android.hardware.camera'
168
+ feature-group: label=''
169
+ uses-feature: name='android.hardware.faketouch'
170
+ ```
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Размещение маппинга в KTX-файлах, один файл на модель, числовые сравнения Build.VERSION.SDK_INT
4
+ вместо буквенных констант
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила архитектуры
10
-
11
11
  - Помещай логику маппинга в KTX-файлы мапперов.
12
12
  - Для каждой модели создавай отдельный файл; не объявляй несколько классов моделей в одном файле.
13
13
  - Для проверок уровня Android API сравнивай `Build.VERSION.SDK_INT` с числовыми уровнями API; не
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Bottom sheet фичи с постфиксом _sheet, rememberModalBottomSheetState(skipPartiallyExpanded =
4
+ true), sheetState.hide() перед intent, preview в Box с fillMaxSize
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила Bottom Sheet
10
-
11
11
  - Пакеты bottom sheet фич в `features` используют постфикс `_sheet`, например
12
12
  `features/profile_sheet`.
13
13
  - Используй file-level `@file:OptIn(ExperimentalMaterial3Api::class)`, когда файл использует
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Цвета только через MaterialTheme.colorScheme, новые цвета в Colors UI kit по HTML-имени hex, цвета
4
+ TopAppBar через TopAppBarDefaults
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила цвета Compose
10
-
11
11
  - Используй цвета через `MaterialTheme.colorScheme`; не используй сырые константы `Color` для
12
12
  тематических цветов внутри компонентов.
13
13
  - Добавляй новые UI-цвета в файл Colors UI kit и предоставляй их через `MaterialTheme.colorScheme`;
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Порядок ограничений в constrainAs (width, height, start, top, end, bottom) и размеры через API
4
+ Dimension вместо внешних modifier
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила ConstraintLayout
10
-
11
11
  - В блоке `constrainAs` упорядочивай присваивания ограничений так: `width`, `height`, `start`,
12
12
  `top`, `end`, `bottom`.
13
13
  - Когда composable позиционируется через `constrainAs`, объявляй его размер внутри блока
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Выбор AlertDialog vs BasicAlertDialog по наличию кнопок действий, слоты icon/title/text,
4
+ оформление кнопок подтверждения и отмены
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила Dialog
10
-
11
11
  - Используй `AlertDialog`, когда в диалоге есть кнопки действий; используй `BasicAlertDialog`, когда
12
12
  кнопок действий нет.
13
13
  - Для `AlertDialog` передавай слот `icon`, когда нужна иконка; устанавливай
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ contentPadding из Scaffold через оператор +, Arrangement.spacedBy вместо Spacer между элементами,
4
+ завершающий Spacer только в конце списка
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила LazyList
10
-
11
9
  - Передавай `innerPadding` из `Scaffold` в `contentPadding`, а не как `Modifier.padding`; комбинируй
12
10
  с дополнительными отступами через оператор `+`:
13
11
  `contentPadding = innerPadding + PaddingValues(bottom = 72.dp)`.
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Skeleton-загрузка через Modifier.placeholder с PlaceholderHighlight.shimmer, Spacer vs Box для
4
+ placeholder, visible привязан к state.isLoading
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила Shimmer / Loading Placeholder
10
-
11
9
  - Используй
12
10
  `Modifier.placeholder(visible = ..., highlight = PlaceholderHighlight.shimmer(), color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(...))`
13
11
  для skeleton-загрузки.
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Preview через @PreviewWrapper(ThemeWrapper::class), вызов приватного *Content, одна функция
4
+ preview с PreviewParameterProvider на все значимые состояния
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила Preview
10
-
11
9
  - Всегда аннотируй previews через `@PreviewWrapper(ThemeWrapper::class)`.
12
10
  - Preview должны вызывать приватный composable `*Content`, а не публичный composable `*Screen`,
13
11
  принимающий ViewModel.
@@ -25,6 +23,9 @@ paths:
25
23
  состояние по умолчанию/первое.
26
24
  - Используй `BooleanProvider` для preview с булевыми параметрами.
27
25
  - Классы `PreviewParameterProvider` приватные и объявляются в конце файла.
26
+ - Вспомогательные значения, которые использует только `PreviewParameterProvider` (списки сущностей,
27
+ отдельные экземпляры), объявляй приватными свойствами внутри самого класса провайдера перед
28
+ `values`, а не на уровне файла.
28
29
  - Строй значения preview и `PreviewParameterProvider` через `Empty.copy(...)`, когда модель
29
30
  предоставляет экземпляр `Empty`; не создавай их полным явным вызовом конструктора.
30
31
  - В вызовах `Empty.copy(...)` для preview устанавливай только те поля, которые компонент
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Размещение обычного и ошибочного snackbar внутри и вне Scaffold, SnackbarMessage vs
4
+ SnackbarErrorMessage, раздельные SnackbarHostState
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила Scaffold
10
-
11
9
  - Snackbar для обычных сообщений размещай внутри `snackbarHost` в `Scaffold`; snackbar, которые
12
10
  должны появляться над статус-баром (верхние баннеры ошибок), размещай вне `Scaffold` в
13
11
  оборачивающем `Box`, выровненном по `Alignment.TopCenter`, с `Modifier.statusBarsPadding()`.
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Разделение публичного {Feature}Screen и приватного {Feature}ScreenContent, сбор state и событий в
4
+ публичном, отсутствие бизнес-логики в Content
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила Compose-экранов
10
-
11
11
  - Публичный `{Feature}Screen(viewModel = hiltViewModel())` собирает state, создаёт
12
12
  remembered-хелперы событий, наблюдает события и делегирует UI приватному `{Feature}ScreenContent`.
13
13
  - `*ScreenContent` получает `state`, `dispatch` и любые remembered UI-хелперы, нужные для preview.
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Порядок именованных параметров padding и PaddingValues по сигнатуре метода, пропуск аргументов
4
+ равных 0.dp
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила отступов и размеров Compose
10
-
11
11
  - Для `padding(...)` и `PaddingValues(...)` упорядочивай именованные параметры согласно сигнатуре
12
12
  метода: `start`, `top`, `end`, `bottom`; либо при использовании сокращённой формы — `horizontal`,
13
13
  `vertical`.
@@ -1,13 +1,9 @@
1
1
  ---
2
+ description: Поля поиска хранят TextFieldValue с явно заданным selection в конце текста, а не String
2
3
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
4
  - "**/*.kt"
7
5
  ---
8
6
 
9
- # Правила TextField
10
-
11
7
  - Для `TextField`, являющихся полями поиска, храни значение как `TextFieldValue`, а не как `String`,
12
8
  и всегда явно устанавливай `selection`, чтобы курсор находился в конце текста, а не в начале:
13
9
  ```kotlin
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Стиль текста только через аргумент style, база MaterialTheme.typography с .copy, визуальные поля
4
+ внутри style а не отдельными аргументами Text
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила типографики
10
-
11
9
  - Всегда устанавливай стиль текста через аргумент `style` в `Text` или `SharedFixedText`; никогда
12
10
  не используй отдельные аргументы `fontSize`, `fontWeight` или `color`.
13
11
  - Используй расширения `MaterialTheme.typography.<token>` как базовый стиль, затем применяй
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Обязательный preview для каждого composable, modifier первым опциональным параметром,
4
+ {Component}State при более чем одном поле данных или callback
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила Compose
10
-
11
11
  - При создании composable-функции всегда создавай для неё preview.
12
12
  - Параметр `modifier` должен быть первым опциональным параметром в сигнатуре composable-функции.
13
13
  - Когда composable-компоненту нужно больше одного поля данных или callback, перенеси эти поля в
@@ -17,6 +17,10 @@ paths:
17
17
  вместо этого предоставляй его как вычисляемое свойство с явным типом и геттером на самом
18
18
  `{Component}State` data class, и читай это свойство напрямую в composable вместо пересчёта условия
19
19
  inline.
20
+ - Не используй `nullableField?.let { ... }` как условие отображения дочернего composable. Заведи на
21
+ `{Component}State` вычисляемое булево свойство (`val isXxxVisible: Boolean get() = field != null`) и
22
+ оборачивай разметку в `if (state.isXxxVisible) { ... }`, а внутри ветки читай nullable-поле
23
+ напрямую, при необходимости через `requireNotNull(state.field)`.
20
24
  - Composable-экраны и компоненты не должны содержать бизнес-логику, ветвящиеся функции принятия
21
25
  решений или `if`/`else`, определяющие domain-поведение; переноси эту логику в ViewModel и рендери
22
26
  уже подготовленное UI-состояние.
@@ -28,6 +32,12 @@ paths:
28
32
  другими правилами, например `*ScreenContent`.
29
33
  - Используй обёртку компонента `Shared*` проекта вместо прямого компонента фреймворка, когда такая
30
34
  обёртка существует в проекте.
35
+ - Не используй `Surface` в простых компонентах вроде карточек, плашек и контейнеров с фоном и
36
+ скруглением. Задавай форму и фон напрямую на `Modifier` контейнера (`Box`, `Column`, `Row`) через
37
+ `clip(RoundedCornerShape(...))` и `background(...)`, а внутренние отступы — через `padding(...)`.
38
+ `Surface` допустим только там, где реально нужны его возможности: интерактивные поверхности с
39
+ `onClick`, `selected` или `enabled`, tonal/shadow elevation, camera preview и подобные
40
+ нетривиальные случаи.
31
41
  - В composable используй `val resources = LocalResources.current`; не используй
32
42
  `val resources = LocalContext.current.resources`.
33
43
  - Вызовы composable с именованными аргументами всегда форматируй многострочно: имя composable,
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Domain-операции в классах UseCase и FlowUseCase в shared/domain/usecase, инъекция
4
+ Room/Ktor/DataStore в конструктор, обработка ответов через handleResponse
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила Domain
10
-
11
11
  - Операции domain-слоя находятся в конкретных классах `UseCase` и `FlowUseCase` в
12
12
  `shared/domain/usecase`.
13
13
  - Зависимости Room, Ktor, DataStore и бизнес-логики внедряются напрямую в конструктор use case.
@@ -1,3 +1,5 @@
1
- # Правила проекта
1
+ ---
2
+ description: При удалении файлов удалять и все директории, которые в результате становятся пустыми
3
+ ---
2
4
 
3
5
  - При удалении файлов также удаляй все директории, которые в результате становятся пустыми.
@@ -1,4 +1,8 @@
1
- # Правила Git
1
+ ---
2
+ description: >-
3
+ Целевые коммиты по одному логическому изменению, коммит всех изменений по запросу пользователя,
4
+ обновления зависимостей форматом Bump <dep> from <old> to <new>
5
+ ---
2
6
 
3
7
  - Никогда не коммить несвязанные файлы вместе; добавляй файлы в индекс выборочно и делайте один
4
8
  целевой коммит на каждое логическое изменение с понятным, конкретным сообщением.
@@ -1,4 +1,8 @@
1
- # Правила GitHub README
1
+ ---
2
+ description: >-
3
+ Фиксированная структура README: бейдж last-commit, описание в два предложения, скриншот, раздел
4
+ Technologies с shields.io for-the-badge бейджами
5
+ ---
2
6
 
3
7
  Каждый README следует этой фиксированной структуре в этом порядке:
4
8
 
@@ -1,4 +1,8 @@
1
- # Правила структуры репозитория
1
+ ---
2
+ description: >-
3
+ Обязательная структура GitHub-репозитория: FUNDING.yml, CODEOWNERS, .idea/icon.svg, whitelist
4
+ .gitignore, AGENTS.md с симлинками CLAUDE.md и GEMINI.md
5
+ ---
2
6
 
3
7
  Каждый GitHub-репозиторий должен следовать этой структуре.
4
8
 
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Маршруты как @Serializable data class или data object реализующие NavKey в
4
+ features/{feature}/navigation, аргументы через savedStateHandle.toRoute
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила навигации
10
-
11
9
  - Каждый маршрут экрана — это `@Serializable data class` или `@Serializable data object`,
12
10
  реализующий `NavKey`.
13
11
  - Маршруты находятся в `features/{feature}/navigation`.
@@ -1,15 +1,17 @@
1
1
  ---
2
+ description: >-
3
+ Порядок методов DAO (fun перед suspend fun), рост DATABASE_VERSION, алфавитный порядок entity и
4
+ DAO, primaryKeys в @Entity, withTransaction на уровне use case
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила Room
10
-
11
9
  - В интерфейсах DAO размещай все обычные методы `fun` перед любыми методами `suspend fun`.
12
10
  - При изменении таблиц или entity базы данных Room всегда увеличивай `AppDatabase.DATABASE_VERSION`.
11
+ - Держи Room-объекты в алфавитном порядке (A–Z) в списке `entities` аннотации `@Database`, в
12
+ объявлениях `abstract fun ...Dao()` внутри `AppDatabase` и в `@Provides`-методах, поставляющих DAO
13
+ в DI-модуле. При добавлении, изменении или удалении entity либо DAO проверяй и восстанавливай этот
14
+ порядок во всех трёх местах.
13
15
  - Для entity Room объявляй первичные ключи в параметре `@Entity(primaryKeys = [...])` вместо
14
16
  использования `@PrimaryKey` на свойстве.
15
17
  - В интерфейсах `@Dao` помечай методы, возвращающие типы Pojo, аннотацией `@Transaction`, чтобы
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Тонкий CoroutineWorker вызывающий бизнес-UseCase через getOrThrow, планирование и отмена в
4
+ отдельных UseCase, constraints, unique work и retry policy
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила WorkManager
10
-
11
9
  - Реализуй фоновую задачу как тонкий `CoroutineWorker`: worker читает и проверяет входные данные,
12
10
  вызывает существующий бизнес-`UseCase` через `.getOrThrow()` и преобразует итог в
13
11
  `Result.success()`, `Result.retry()` или `Result.failure()`. Для чисто Android-системной операции,
@@ -1,13 +1,13 @@
1
1
  ---
2
+ description: >-
3
+ Синхронное обновление версии приложения во всех таргетах: androidApp, desktopApp, iosAppCompose и
4
+ iosApp Version.xcconfig
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
7
  - "**/AndroidManifest.xml"
6
8
  - "**/*.kt"
7
9
  ---
8
10
 
9
- # Правила KMP
10
-
11
11
  - При обновлении версии приложения обновляй её во всех таргетах: `androidApp/build.gradle.kts`
12
12
  (`versionName`), `desktopApp/build.gradle.kts` (`desktopVersionName`),
13
13
  `iosAppCompose/build.gradle.kts` (`iosVersionName`) и `iosApp/Configuration/Version.xcconfig`
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: >-
3
+ Запрет GlobalScope, инъекция CoroutineDispatcher в конструктор вместо хардкода, обёртка тела ветки
4
+ when в фигурные скобки перед launch
5
+ paths:
6
+ - "**/*.gradle.kts"
7
+ - "**/*.kt"
8
+ ---
9
+
10
+ - Не используй `GlobalScope`; запускай корутины только в scope с определённым жизненным циклом
11
+ (переданный `CoroutineScope`, scope MVI-стора).
12
+ - Не хардкодь `Dispatchers.IO`/`Dispatchers.Default`/`Dispatchers.Main` внутри классов; принимай
13
+ `CoroutineDispatcher` параметром конструктора, чтобы тест подставлял `TestDispatcher`.
14
+ - В ветках `when` не ставь `launch` сразу после `->`; оборачивай тело ветки в фигурные скобки и
15
+ вызывай `launch { ... }` внутри блока ветки, даже если `launch` — единственная инструкция.
@@ -1,18 +1,19 @@
1
1
  ---
2
+ description: >-
3
+ Длина строки до 150 символов, when вместо цепочек if/else if, return when, fail-fast ранним
4
+ return/throw вместо вложенных if/else, однострочные ветки when только при полном размещении
2
5
  paths:
3
6
  - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
7
  - "**/*.kt"
7
8
  ---
8
9
 
9
- # Правила Kotlin
10
-
11
10
  - Допускай длину строки Kotlin-кода до 150 символов; не переноси код только ради стандартного
12
11
  ограничения IDE в 100 символов.
13
12
  - Предпочитай `when` вместо цепочек `if / else if` при выражении ветвящейся логики.
14
13
  - При возврате одной из двух веток предпочитай `return when { ... }` вместо
15
14
  `return if (...) ... else ...`.
15
+ - Применяй принцип fail-fast: отсеивай невалидные и граничные случаи ранним `return`, `throw` или
16
+ `require` / `check` в начале функции, а не оборачивай основную логику во вложенные `if / else`.
16
17
  - В ветках `when` используй однострочную форму без фигурных скобок только если вся ветка помещается
17
18
  на одной строке: `condition -> statement`; если тело ветки многострочное, оборачивай его в
18
19
  фигурные скобки, даже если оно содержит только одну инструкцию.
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Импорт вложенных исключений use case во ViewModel, порядок catch от специфичных сетевых к общим,
4
+ сброс Job запроса и SnackbarErrorMessage
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила обработки ошибок в MVI
10
-
11
9
  - Импортируй вложенные исключения use case напрямую в ViewModel, например
12
10
  `import shared.domain.usecase.ItemsDetailsUseCase.ItemsDetailsException`.
13
11
  - В `catch` ViewModel обрабатывай специфичное сетевое исключение фичи раньше более общих исключений:
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Room Entity напрямую в Model и composable без промежуточных UI-моделей, имена ...Entity и
4
+ ...Entities, производные свойства с кастомным геттером
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила Model в MVI
10
-
11
9
  - Для данных, поддерживаемых Room, храни и передавай класс `Entity` из Room напрямую в классах
12
10
  `Model` фичи и composable-компонентах; не маппи его в UI-модель или другой промежуточный класс.
13
11
  - В классах `Model` фичи называй свойства Room entity по форме entity: используй `...Entity` для
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Раскладка features/{feature} на Screen/ViewModel/Model/Intent/Event/Route, @HiltViewModel с
4
+ конструкторной инъекцией, обязательные MVI-классы для каждого экрана
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила MVI
10
-
11
9
  - Экраны фич находятся в `features/{feature}` и разбиты на `{Feature}Screen.kt`,
12
10
  `{Feature}ViewModel.kt`, `model/{Feature}Model.kt`, `intent/{Feature}Intent.kt`, опционально
13
11
  `event/{Feature}Event.kt` и опционально `navigation/{Feature}Route.kt`.
@@ -31,6 +29,8 @@ paths:
31
29
  ветку `dispatch` либо выноси их из ViewModel в соответствующий архитектурный слой.
32
30
  - `dispatch` — это `when` по всем веткам intent без `else`; состояние меняется только через
33
31
  `reduce { it.copy(...) }`.
32
+ - Не именуй параметр лямбды `reduce`: пиши `reduce { it.copy(...) }`, а не
33
+ `reduce { state -> state.copy(...) }`.
34
34
  - Одноразовые действия используют `send({Feature}Event...)` из ViewModel и `ObserveAsEvents` на
35
35
  экране.
36
36
  - Типы `Intent`, `Model` и `Event` реализуют общие маркерные интерфейсы MVI проекта.
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Имена моделей на Request и Response, @Serializable и @SerialName на каждом поле, имена сетевых use
4
+ case по пути запроса в PascalCase плюс UseCase
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила сети
10
-
11
9
  - Имена классов моделей запросов должны заканчиваться на `Request`.
12
10
  - Имена классов моделей ответов должны заканчиваться на `Response`.
13
11
  - Каждая модель запроса и ответа должна быть аннотирована `@Serializable`, а каждое поле должно
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Отдельный RealtimeDataSource на каждый hub или канал, общая инфраструктура соединения, регистрация
4
+ обработчиков до подключения, conflatable invalidation через SharedFlow
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила realtime-соединений
10
-
11
9
  - Создавай отдельный `RealtimeDataSource` для каждого hub/канала предметной области. Общие
12
10
  создание соединения, аутентификацию, retry/backoff, логирование и закрытие держи в общей
13
11
  инфраструктуре соединения.
@@ -1,13 +1,12 @@
1
1
  ---
2
+ description: >-
3
+ UI-строки только через фасад строк проекта без прямого R.string/R.plurals и хардкода в Kotlin или
4
+ XML, новые строки в strings.xml
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
+ - "**/*.xml"
6
7
  - "**/*.kt"
7
8
  ---
8
9
 
9
- # Правила ресурсов
10
-
11
10
  - Строки UI используются через фасад строк проекта, а не через прямое использование `R.string` или
12
11
  `R.plurals` в UI-коде.
13
12
  - Не хардкодь пользовательские строки напрямую в Kotlin или XML UI-коде; добавляй их в `strings.xml`
@@ -0,0 +1,112 @@
1
+ ---
2
+ description: >-
3
+ Базовые требования безопасности из OWASP MASVS и Mobile Top 10 (2024): секреты, хранение, сеть,
4
+ криптография, IPC, логирование
5
+ paths:
6
+ - "**/*.kt"
7
+ ---
8
+
9
+ Базовые требования по безопасности для Kotlin/Android/KMP-кода, сжатые из OWASP MASVS и Mobile
10
+ Top 10 (2024). Это стандарты, которые соблюдаются при написании кода; полный доказательный
11
+ аудит с threat model и эксплуатационными сценариями делает агент `security-auditor`, массовый
12
+ проход по модулям — workflow `security-sweep`.
13
+
14
+ ## Секреты и конфигурация сборки
15
+
16
+ - Не коммить ключи, токены, пароли, keystore и `google-services.json` с реальными ключами; храни
17
+ их в `local.properties` (в `.gitignore`), в переменных окружения CI или в секрет-хранилище.
18
+ - Не клади секреты в `BuildConfig` и в ресурсы: значения из `BuildConfig` тривиально достаются из
19
+ APK. Для клиентских идентификаторов, которые всё равно попадают в бинарь, полагайся на
20
+ серверную проверку, а не на «скрытость».
21
+ - `.gitignore` должен содержать `local.properties`, `*.jks`, `*.keystore`, `*.p12`, `*.pem`,
22
+ `secrets*.properties`.
23
+ - В KMP секреты не размещай в `commonMain`; платформенные ключи подаются через `expect/actual`
24
+ провайдер, который на каждой платформе читает из защищённого хранилища.
25
+
26
+ ## Транспорт
27
+
28
+ - Только HTTPS. В `AndroidManifest.xml` держи `android:usesCleartextTraffic="false"`; исключения
29
+ оформляй через `network_security_config.xml` с явным списком доменов и только для отладки.
30
+ - Certificate pinning для собственного backend:
31
+ - OkHttp — `CertificatePinner` с pin-set из SPKI SHA-256 leaf/intermediate сертификата.
32
+ - Ktor (Darwin/OkHttp engine) — pinning на уровне engine-конфига соответствующей платформы.
33
+ - Всегда закладывай минимум два пина (текущий + запасной) и документируй дату ротации; пин без
34
+ запасного превращает продление сертификата в отказ обслуживания.
35
+ - Не отключай проверку хоста и не подставляй `TrustManager`, принимающий все сертификаты, даже
36
+ во flavor'ах для тестирования, которые могут случайно уехать в релиз.
37
+
38
+ ## Хранение данных
39
+
40
+ - Чувствительные данные (токены сессии, PII) — только в шифрованном хранилище:
41
+ `EncryptedSharedPreferences` / шифрованный `DataStore` с ключом из `MasterKey`
42
+ (`AES256_GCM`), либо Android Keystore напрямую.
43
+ - Не пиши чувствительные данные в обычный `SharedPreferences`, в файлы на внешнем хранилище, в
44
+ имена файлов кэша и в логи.
45
+ - Room-база с чувствительными данными — SQLCipher или хранение только зашифрованных значений;
46
+ ключ БД не хардкодь.
47
+ - `android:allowBackup` — выключай (`false`) либо задавай `fullBackupContent` / `dataExtractionRules`,
48
+ исключающие секретные файлы, иначе токены утекут в облачный бэкап.
49
+
50
+ ## Манифест и экспорт компонентов
51
+
52
+ - Каждому `activity`/`service`/`receiver`/`provider` явно проставляй `android:exported`;
53
+ `exported="true"` без строгой проверки вызывающего — потенциальный вектор.
54
+ - Экспортированные компоненты, принимающие `Intent`, валидируют все extras и `data`-URI;
55
+ `ContentProvider` с `grantUriPermissions` — только на конкретные пути; deep links не дают
56
+ доступ к привилегированным экранам без проверки авторизации.
57
+ - Детальный разбор Intent-поверхности (redirection, implicit intents, `PendingIntent`
58
+ mutability) — скилл `android-intent-security` из маркетплейса `android-skills`.
59
+
60
+ ## Логирование и релизный билд
61
+
62
+ - В релизе не логируй PII, токены, тела запросов/ответов. ProGuard/R8-правило вырезает `Log.*` и
63
+ `println` из релиза:
64
+
65
+ ```proguard
66
+ -assumenosideeffects class android.util.Log {
67
+ public static int v(...);
68
+ public static int d(...);
69
+ public static int i(...);
70
+ public static int w(...);
71
+ public static int e(...);
72
+ }
73
+ ```
74
+
75
+ - Релизный билд: `isMinifyEnabled = true`, `isShrinkResources = true`, R8 full mode
76
+ (`android.enableR8.fullMode=true`). Проверяй, что `mapping.txt` сгенерирован и загружен в
77
+ Play Console / crash-репортер.
78
+ - `isDebuggable` в релизе — только `false`. `BuildConfig.DEBUG`-ветки не должны открывать
79
+ тестовые бэкдоры в релизной конфигурации.
80
+
81
+ ## Аутентификация и криптография
82
+
83
+ - Не изобретай криптографию: `javax.crypto` / Tink / платформенные API. AES только в режиме с
84
+ аутентификацией (`GCM`), IV — случайный на каждое сообщение, не переиспользуется.
85
+ - Биометрия — `BiometricPrompt` с `CryptoObject`, привязанным к ключу Keystore
86
+ (`setUserAuthenticationRequired(true)`); «биометрия ради галочки» без `CryptoObject` не
87
+ защищает данные.
88
+ - Play Integrity API для проверки целостности клиента там, где это оправдано: nonce генерируется
89
+ на сервере, ответ верифицируется на сервере, вердикт не кэшируется на клиенте.
90
+ - Токены сессии — короткоживущие, обновляются по refresh-токену; логаут инвалидирует токены и на
91
+ сервере, и в локальном хранилище.
92
+
93
+ ## Supply chain
94
+
95
+ - Версии зависимостей — только через version catalog; не тяни динамические версии (`+`,
96
+ `latest.release`).
97
+ - Включай dependency verification (`gradle/verification-metadata.xml`) или как минимум
98
+ dependency review в CI.
99
+ - Плагины Gradle и GitHub Actions пинуй: экшены — по полному commit SHA, не по mutable-тегу.
100
+
101
+ ## Чек-лист перед релизом
102
+
103
+ - [ ] в диффе и в истории нет секретов (`git log -p`, secret-scanner)
104
+ - [ ] `usesCleartextTraffic=false`, network security config без общих исключений
105
+ - [ ] certificate pinning для собственного backend, есть запасной пин
106
+ - [ ] чувствительные данные только в шифрованном хранилище
107
+ - [ ] `allowBackup=false` или бэкап-правила исключают секреты
108
+ - [ ] у всех компонентов явный `exported`, экспортированные валидируют вход
109
+ - [ ] R8 full mode, `Log.*` вырезан, `mapping.txt` сохранён
110
+ - [ ] `isDebuggable=false`, нет debug-бэкдоров
111
+ - [ ] биометрия через `CryptoObject`, крипто — только аутентифицированные режимы
112
+ - [ ] версии зависимостей зафиксированы, экшены пиннятся по SHA
@@ -1,13 +1,11 @@
1
1
  ---
2
+ description: >-
3
+ Операции Room/Ktor/DataStore и бизнес-логика в классах UseCase и FlowUseCase в
4
+ shared/domain/usecase без слоёв Repository или Interactor, конструкторная инъекция
2
5
  paths:
3
- - "**/*.gradle.kts"
4
- - "**/*.gradle"
5
- - "**/AndroidManifest.xml"
6
6
  - "**/*.kt"
7
7
  ---
8
8
 
9
- # Правила UseCase
10
-
11
9
  - Размещай операции Room, Ktor, DataStore и бизнес-операции в конкретных классах `UseCase` /
12
10
  `FlowUseCase` в `shared/domain/usecase`; не создавай слои Repository или Interactor для новой
13
11
  работы.
@@ -1,4 +1,8 @@
1
- # Правила рабочего процесса
1
+ ---
2
+ description: >-
3
+ Соглашения проекта, дублирование вместо преждевременных абстракций, делегирование реализации
4
+ профильным саб-агентам, крупные задачи через фоновый агент в worktree
5
+ ---
2
6
 
3
7
  - Используй стандартные соглашения проекта и следуй существующей документации при внесении
4
8
  изменений.
@@ -31,18 +31,23 @@ lower camel case, а `{package}` на целевой пакет.
31
31
 
32
32
  Файлы:
33
33
  - `features/{feature}_sheet/{Feature}BottomSheet.kt`
34
- - `features/{feature}_sheet/intent/{Feature}SheetIntent.kt`
35
- - optional `features/{feature}_sheet/model/{Feature}SheetModel.kt`
34
+ - `features/{feature}_sheet/intent/{Feature}Intent.kt`
35
+ - optional `features/{feature}_sheet/model/{Feature}Model.kt`
36
36
 
37
- ### {Feature}SheetIntent.kt
37
+ MVI-типы sheet называются `{Feature}Intent` и `{Feature}Model` — без инфикса `Sheet`. Инфикс
38
+ `Sheet` остаётся только в имени пакета (`{feature}_sheet`), в composable (`{Feature}BottomSheet`) и
39
+ в идентификаторах на стороне вызывающего экрана (`is{Feature}SheetVisible`, `On{Feature}SheetIntent`,
40
+ `{feature}SheetState`). Собственный `{Feature}ViewModel` у sheet не создаётся — см. Фазу 3.
41
+
42
+ ### {Feature}Intent.kt
38
43
 
39
44
  ```kotlin
40
45
  package {package}.features.{feature}_sheet.intent
41
46
 
42
47
  import {package}.shared.mvi.Intent
43
48
 
44
- sealed interface {Feature}SheetIntent: Intent {
45
- data object DismissClick: {Feature}SheetIntent
49
+ sealed interface {Feature}Intent: Intent {
50
+ data object DismissClick: {Feature}Intent
46
51
  }
47
52
  ```
48
53
 
@@ -51,14 +56,14 @@ sealed interface {Feature}SheetIntent: Intent {
51
56
  - Для остальных действий (клик по элементу, primary/secondary action и т. д.) добавляй свои
52
57
  `data object` / `data class` записи; `data object` всегда идёт перед `data class`.
53
58
 
54
- ### {Feature}SheetModel.kt (только если есть данные)
59
+ ### {Feature}Model.kt (только если есть данные)
55
60
 
56
61
  ```kotlin
57
62
  package {package}.features.{feature}_sheet.model
58
63
 
59
64
  import {package}.shared.mvi.Model
60
65
 
61
- data class {Feature}SheetModel(
66
+ data class {Feature}Model(
62
67
  val showPrimaryAction: Boolean = false
63
68
  ): Model
64
69
  ```
@@ -72,11 +77,11 @@ package {package}
72
77
 
73
78
  @Composable
74
79
  fun {Feature}BottomSheet(
75
- state: {Feature}SheetModel,
76
- dispatch: ({Feature}SheetIntent) -> Unit
80
+ state: {Feature}Model,
81
+ dispatch: ({Feature}Intent) -> Unit
77
82
  ) {
78
83
  SharedModalBottomSheet(
79
- onDismissRequest = { dispatch({Feature}SheetIntent.DismissClick) }
84
+ onDismissRequest = { dispatch({Feature}Intent.DismissClick) }
80
85
  ) {
81
86
  SharedLazyColumn(
82
87
  modifier = Modifier.fillMaxWidth(),
@@ -93,21 +98,21 @@ fun {Feature}BottomSheet(
93
98
  {Feature}Card(
94
99
  state = {Feature}CardState(
95
100
  model = state.item,
96
- onClick = { dispatch({Feature}SheetIntent.ItemClick) }
101
+ onClick = { dispatch({Feature}Intent.ItemClick) }
97
102
  )
98
103
  )
99
104
  }
100
105
  if (state.showSecondaryAction) {
101
106
  item {
102
107
  {Feature}SecondaryButton(
103
- onClick = { dispatch({Feature}SheetIntent.SecondaryActionClick) }
108
+ onClick = { dispatch({Feature}Intent.SecondaryActionClick) }
104
109
  )
105
110
  }
106
111
  }
107
112
  if (state.showPrimaryAction) {
108
113
  item {
109
114
  Button(
110
- onClick = { dispatch({Feature}SheetIntent.PrimaryActionClick) },
115
+ onClick = { dispatch({Feature}Intent.PrimaryActionClick) },
111
116
  modifier = Modifier
112
117
  .fillMaxWidth()
113
118
  .height(56.dp),
@@ -130,7 +135,7 @@ fun {Feature}BottomSheet(
130
135
  @Preview
131
136
  @Composable
132
137
  private fun {Feature}BottomSheetPreview(
133
- @PreviewParameter({Feature}SheetModelPreviewParameterProvider::class) state: {Feature}SheetModel
138
+ @PreviewParameter({Feature}ModelPreviewParameterProvider::class) state: {Feature}Model
134
139
  ) {
135
140
  Box(
136
141
  modifier = Modifier.fillMaxSize()
@@ -142,8 +147,8 @@ private fun {Feature}BottomSheetPreview(
142
147
  }
143
148
  }
144
149
 
145
- private class {Feature}SheetModelPreviewParameterProvider: PreviewParameterProvider<{Feature}SheetModel> {
146
- override val values: Sequence<{Feature}SheetModel>
150
+ private class {Feature}ModelPreviewParameterProvider: PreviewParameterProvider<{Feature}Model> {
151
+ override val values: Sequence<{Feature}Model>
147
152
  get() {
148
153
  val item = {Feature}ItemModel(
149
154
  id = "sample-id",
@@ -152,12 +157,12 @@ private class {Feature}SheetModelPreviewParameterProvider: PreviewParameterProvi
152
157
  )
153
158
 
154
159
  return sequenceOf(
155
- {Feature}SheetModel(
160
+ {Feature}Model(
156
161
  item = item,
157
162
  showPrimaryAction = true,
158
163
  showSecondaryAction = false
159
164
  ),
160
- {Feature}SheetModel(
165
+ {Feature}Model(
161
166
  item = item,
162
167
  showPrimaryAction = false,
163
168
  showSecondaryAction = true
@@ -210,11 +215,11 @@ reduce { it.copy(is{Feature}SheetVisible = true) }
210
215
  ### 3. Обработка intent'ов sheet в ViewModel экрана
211
216
 
212
217
  Добавь в `{Screen}Intent` экрана один case `On{Feature}SheetIntent`, оборачивающий весь
213
- `{Feature}SheetIntent` целиком — отдельный case на каждый intent sheet не создавай:
218
+ `{Feature}Intent` целиком — отдельный case на каждый intent sheet не создавай:
214
219
 
215
220
  ```kotlin
216
221
  sealed interface {Screen}Intent: Intent {
217
- data class On{Feature}SheetIntent(val intent: {Feature}SheetIntent): {Screen}Intent
222
+ data class On{Feature}SheetIntent(val intent: {Feature}Intent): {Screen}Intent
218
223
  }
219
224
  ```
220
225
 
@@ -225,7 +230,7 @@ override fun dispatch(intent: {Screen}Intent) {
225
230
  when (intent) {
226
231
  is {Screen}Intent.On{Feature}SheetIntent -> {
227
232
  when (intent.intent) {
228
- is {Feature}SheetIntent.DismissClick -> {
233
+ is {Feature}Intent.DismissClick -> {
229
234
  reduce { it.copy(is{Feature}SheetVisible = false) }
230
235
  }
231
236
  }
@@ -265,15 +270,15 @@ fun {Screen}Screen(
265
270
 
266
271
  Когда sheet принимает `state`, не храни отдельную копию его Model в экране — вычисляй её свойством
267
272
  `get()` из уже существующих данных экрана. Имя свойства оканчивается на `State`, а не на `Model`
268
- (сам тип остаётся `{Feature}SheetModel`):
273
+ (сам тип остаётся `{Feature}Model`):
269
274
 
270
275
  ```kotlin
271
276
  data class {Screen}Model(
272
277
  val {feature}Item: {Feature}Item = {Feature}Item.Empty,
273
278
  val is{Feature}SheetVisible: Boolean = false
274
279
  ): Model {
275
- val {feature}SheetState: {Feature}SheetModel
276
- get() = {Feature}SheetModel(
280
+ val {feature}SheetState: {Feature}Model
281
+ get() = {Feature}Model(
277
282
  item = {feature}Item
278
283
  )
279
284
  }
@@ -291,8 +296,8 @@ if (state.is{Feature}SheetVisible) {
291
296
  Правила:
292
297
  - Sheet не хранит собственный ViewModel; видимостью управляет `Boolean`-поле
293
298
  `is{Feature}SheetVisible` в `Model` вызывающего экрана.
294
- - Экран оборачивает весь `{Feature}SheetIntent` в один case `On{Feature}SheetIntent(val intent:
295
- {Feature}SheetIntent)` своего `{Screen}Intent`; отдельных case'ов на каждый intent sheet не
299
+ - Экран оборачивает весь `{Feature}Intent` в один case `On{Feature}SheetIntent(val intent:
300
+ {Feature}Intent)` своего `{Screen}Intent`; отдельных case'ов на каждый intent sheet не
296
301
  создавай — разбирай их вложенным `when` внутри `dispatch` ViewModel.
297
302
  - Рендери sheet в первой, публичной функции экрана, сразу после вызова
298
303
  `{Screen}ScreenContent(...)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelbel/cuckcoder-mcp",
3
- "version": "1.6.11",
3
+ "version": "1.6.12",
4
4
  "description": "MCP server for Cuckcoder rules and skills",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/dist/github.js DELETED
@@ -1,23 +0,0 @@
1
- const BASE_RAW = "https://raw.githubusercontent.com";
2
- const BASE_API = "https://api.github.com";
3
- export async function fetchFile(owner, repo, branch, path) {
4
- const url = `${BASE_RAW}/${owner}/${repo}/${branch}/${path}`;
5
- const res = await fetch(url);
6
- if (!res.ok) {
7
- throw new Error(`Not found: ${path} (HTTP ${res.status})`);
8
- }
9
- return res.text();
10
- }
11
- export async function listTree(owner, repo, branch) {
12
- const url = `${BASE_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
13
- const res = await fetch(url, {
14
- headers: { Accept: "application/vnd.github.v3+json" },
15
- });
16
- if (!res.ok) {
17
- throw new Error(`Failed to fetch tree (HTTP ${res.status})`);
18
- }
19
- const data = (await res.json());
20
- return data.tree
21
- .filter((item) => item.type === "blob")
22
- .map((item) => item.path);
23
- }