@coralai/sps-cli 0.42.0 → 0.43.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 (109) hide show
  1. package/README.md +34 -3
  2. package/dist/commands/projectInit.d.ts.map +1 -1
  3. package/dist/commands/projectInit.js +40 -53
  4. package/dist/commands/projectInit.js.map +1 -1
  5. package/dist/commands/skillCommand.d.ts +2 -0
  6. package/dist/commands/skillCommand.d.ts.map +1 -0
  7. package/dist/commands/skillCommand.js +235 -0
  8. package/dist/commands/skillCommand.js.map +1 -0
  9. package/dist/core/skillStore.d.ts +46 -0
  10. package/dist/core/skillStore.d.ts.map +1 -0
  11. package/dist/core/skillStore.js +197 -0
  12. package/dist/core/skillStore.js.map +1 -0
  13. package/dist/core/skillStore.test.d.ts +2 -0
  14. package/dist/core/skillStore.test.d.ts.map +1 -0
  15. package/dist/core/skillStore.test.js +190 -0
  16. package/dist/core/skillStore.test.js.map +1 -0
  17. package/dist/main.js +19 -17
  18. package/dist/main.js.map +1 -1
  19. package/package.json +1 -1
  20. package/skills/architecture-decision-records/SKILL.md +207 -0
  21. package/skills/backend/SKILL.md +62 -0
  22. package/skills/backend/references/api-design.md +168 -0
  23. package/skills/backend/references/caching.md +181 -0
  24. package/skills/backend/references/data-access.md +173 -0
  25. package/skills/backend/references/layering.md +181 -0
  26. package/skills/backend/references/observability.md +190 -0
  27. package/skills/backend/references/resilience.md +201 -0
  28. package/skills/backend/references/security.md +186 -0
  29. package/skills/backend-architect/SKILL.md +119 -0
  30. package/skills/code-reviewer/SKILL.md +143 -0
  31. package/skills/coding-standards/SKILL.md +60 -0
  32. package/skills/coding-standards/references/clean-code.md +258 -0
  33. package/skills/coding-standards/references/code-review.md +192 -0
  34. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  35. package/skills/coding-standards/references/error-strategy.md +193 -0
  36. package/skills/coding-standards/references/naming.md +185 -0
  37. package/skills/coding-standards/references/tdd.md +171 -0
  38. package/skills/database/SKILL.md +53 -0
  39. package/skills/database/references/indexing.md +190 -0
  40. package/skills/database/references/migrations.md +199 -0
  41. package/skills/database/references/nosql.md +185 -0
  42. package/skills/database/references/queries.md +295 -0
  43. package/skills/database/references/scaling.md +203 -0
  44. package/skills/database/references/schema.md +191 -0
  45. package/skills/database-optimizer/SKILL.md +168 -0
  46. package/skills/debugging-workflow/SKILL.md +244 -0
  47. package/skills/devops/SKILL.md +55 -0
  48. package/skills/devops/references/ci-cd.md +204 -0
  49. package/skills/devops/references/containers.md +272 -0
  50. package/skills/devops/references/deploy.md +201 -0
  51. package/skills/devops/references/iac.md +252 -0
  52. package/skills/devops/references/observability.md +228 -0
  53. package/skills/devops/references/secrets.md +178 -0
  54. package/skills/devops-automator/SKILL.md +164 -0
  55. package/skills/frontend/SKILL.md +52 -0
  56. package/skills/frontend/references/accessibility.md +222 -0
  57. package/skills/frontend/references/components.md +206 -0
  58. package/skills/frontend/references/performance.md +219 -0
  59. package/skills/frontend/references/routing.md +209 -0
  60. package/skills/frontend/references/state.md +190 -0
  61. package/skills/frontend/references/testing.md +216 -0
  62. package/skills/frontend-developer/SKILL.md +115 -0
  63. package/skills/git-workflow/SKILL.md +355 -0
  64. package/skills/golang/SKILL.md +49 -0
  65. package/skills/golang/references/concurrency.md +284 -0
  66. package/skills/golang/references/errors.md +241 -0
  67. package/skills/golang/references/idioms.md +285 -0
  68. package/skills/golang/references/testing.md +238 -0
  69. package/skills/java/SKILL.md +50 -0
  70. package/skills/java/references/concurrency.md +194 -0
  71. package/skills/java/references/idioms.md +283 -0
  72. package/skills/java/references/testing.md +228 -0
  73. package/skills/kotlin/SKILL.md +47 -0
  74. package/skills/kotlin/references/coroutines.md +240 -0
  75. package/skills/kotlin/references/idioms.md +268 -0
  76. package/skills/kotlin/references/testing.md +219 -0
  77. package/skills/mobile/SKILL.md +50 -0
  78. package/skills/mobile/references/architecture.md +204 -0
  79. package/skills/mobile/references/navigation.md +158 -0
  80. package/skills/mobile/references/performance.md +152 -0
  81. package/skills/mobile/references/platform.md +166 -0
  82. package/skills/mobile/references/state-and-data.md +174 -0
  83. package/skills/python/SKILL.md +51 -0
  84. package/skills/python/THIRD_PARTY.md +14 -0
  85. package/skills/python/references/async.md +218 -0
  86. package/skills/python/references/error-handling.md +254 -0
  87. package/skills/python/references/idioms.md +279 -0
  88. package/skills/python/references/packaging.md +233 -0
  89. package/skills/python/references/testing.md +269 -0
  90. package/skills/python/references/typing.md +292 -0
  91. package/skills/qa-tester/SKILL.md +186 -0
  92. package/skills/rust/SKILL.md +50 -0
  93. package/skills/rust/references/async.md +224 -0
  94. package/skills/rust/references/errors.md +240 -0
  95. package/skills/rust/references/ownership.md +263 -0
  96. package/skills/rust/references/testing.md +274 -0
  97. package/skills/rust/references/traits.md +250 -0
  98. package/skills/security-engineer/SKILL.md +157 -0
  99. package/skills/swift/SKILL.md +48 -0
  100. package/skills/swift/references/concurrency.md +280 -0
  101. package/skills/swift/references/idioms.md +334 -0
  102. package/skills/swift/references/testing.md +229 -0
  103. package/skills/typescript/SKILL.md +51 -0
  104. package/skills/typescript/references/async.md +241 -0
  105. package/skills/typescript/references/errors.md +208 -0
  106. package/skills/typescript/references/idioms.md +246 -0
  107. package/skills/typescript/references/testing.md +225 -0
  108. package/skills/typescript/references/tooling.md +208 -0
  109. package/skills/typescript/references/types.md +259 -0
@@ -0,0 +1,219 @@
1
+ # Kotlin — Testing
2
+
3
+ JUnit 5, Kotest, MockK, turbine for Flow. For TDD, see `coding-standards/references/tdd.md`.
4
+
5
+ ## Runner choice — JUnit 5 is the default
6
+
7
+ Fine for most projects. Kotest is a Kotlin-first alternative with extra styles (BehaviorSpec, FunSpec) — pick one per project and be consistent.
8
+
9
+ ```kotlin
10
+ // JUnit 5
11
+ import org.junit.jupiter.api.Test
12
+ import kotlin.test.assertEquals
13
+
14
+ class UserServiceTest {
15
+ @Test
16
+ fun `creates user with generated id`() {
17
+ val svc = UserService(InMemoryUserRepo())
18
+ val u = svc.create("a@x.com")
19
+ assertEquals("a@x.com", u.email)
20
+ }
21
+ }
22
+ ```
23
+
24
+ Backtick-quoted function names are standard Kotlin test style — they read as behaviour statements.
25
+
26
+ ## Assertions
27
+
28
+ - `kotlin.test.*` for cross-platform (`assertEquals`, `assertTrue`, `assertFailsWith`).
29
+ - `AssertJ` (Java) for rich fluent asserts.
30
+ - `Kotest assertions` (`shouldBe`, `shouldHaveSize`) if you've adopted Kotest.
31
+
32
+ ```kotlin
33
+ // kotlin.test
34
+ assertEquals(5, add(2, 3))
35
+ assertFailsWith<ValidationError> { validate(bad) }
36
+
37
+ // AssertJ
38
+ assertThat(users).hasSize(3).extracting("email").contains("a@x.com")
39
+
40
+ // Kotest
41
+ user.name shouldBe "A"
42
+ users shouldHaveSize 3
43
+ ```
44
+
45
+ Pick one assertion library per project. Mixing creates friction.
46
+
47
+ ## Parameterized tests
48
+
49
+ ```kotlin
50
+ @ParameterizedTest
51
+ @CsvSource("1,2,3", "0,0,0", "-1,1,0")
52
+ fun `add`(a: Int, b: Int, expected: Int) {
53
+ assertEquals(expected, add(a, b))
54
+ }
55
+
56
+ @ParameterizedTest
57
+ @MethodSource("cases")
58
+ fun `login cases`(c: LoginCase) { ... }
59
+
60
+ companion object {
61
+ @JvmStatic
62
+ fun cases() = listOf(
63
+ LoginCase("a@x.com", "pw", true),
64
+ LoginCase("", "pw", false),
65
+ )
66
+ }
67
+ ```
68
+
69
+ Kotest syntax is lighter:
70
+ ```kotlin
71
+ "add" - {
72
+ withData(
73
+ nameFn = { "${it.a} + ${it.b}" },
74
+ Triple(1, 2, 3), Triple(0, 0, 0)
75
+ ) { (a, b, want) -> add(a, b) shouldBe want }
76
+ }
77
+ ```
78
+
79
+ ## Mocking — MockK
80
+
81
+ MockK is Kotlin-idiomatic (final classes work out of the box; Mockito needs extra config).
82
+
83
+ ```kotlin
84
+ import io.mockk.*
85
+
86
+ val repo = mockk<UserRepository>()
87
+ every { repo.find("u1") } returns User("u1", "a@x.com")
88
+ every { repo.find(not("u1")) } returns null
89
+
90
+ val svc = UserService(repo)
91
+ assertEquals("a@x.com", svc.getEmail("u1"))
92
+
93
+ verify(exactly = 1) { repo.find("u1") }
94
+ ```
95
+
96
+ Coroutine mocks:
97
+ ```kotlin
98
+ val svc = mockk<UserService>()
99
+ coEvery { svc.fetch("u1") } returns User("u1", "a@x.com")
100
+ coVerify { svc.fetch("u1") }
101
+ ```
102
+
103
+ `relaxed = true` makes all unconfigured calls return defaults. Use sparingly — silent defaults mask bugs.
104
+
105
+ ## Prefer fakes over mocks
106
+
107
+ ```kotlin
108
+ class InMemoryUserRepo : UserRepository {
109
+ private val users = mutableMapOf<String, User>()
110
+ override fun find(id: String) = users[id]
111
+ override fun save(u: User) { users[u.id] = u }
112
+ }
113
+ ```
114
+
115
+ Reuse across tests; reset in `@BeforeEach`. More code up front; much less friction when the interface grows.
116
+
117
+ ## Coroutine tests — `runTest`
118
+
119
+ ```kotlin
120
+ import kotlinx.coroutines.test.runTest
121
+
122
+ @Test
123
+ fun fetches() = runTest {
124
+ val svc = UserService(fakeRepo)
125
+ val u = svc.fetchUser("u1") // no real delay
126
+ assertEquals("u1", u.id)
127
+ }
128
+ ```
129
+
130
+ `runTest` virtualizes time. `delay(1.hours)` completes instantly. `advanceTimeBy(...)` controls the scheduler.
131
+
132
+ ## `Flow` tests — turbine
133
+
134
+ ```kotlin
135
+ import app.cash.turbine.test
136
+
137
+ @Test
138
+ fun `flow emits expected values`() = runTest {
139
+ viewModel.state.test {
140
+ assertEquals(State.Loading, awaitItem())
141
+ viewModel.load()
142
+ assertEquals(State.Success(user), awaitItem())
143
+ cancelAndIgnoreRemainingEvents()
144
+ }
145
+ }
146
+ ```
147
+
148
+ Don't test Flows by collecting into a list in an uncontrolled scope — tests become flaky.
149
+
150
+ ## Integration tests
151
+
152
+ - **JVM backend**: real DB via Testcontainers; real HTTP via Ktor client or `MockWebServer`.
153
+ - **Android**: instrumented tests (`androidx.test`), or local-JVM Robolectric for fast feedback.
154
+
155
+ ```kotlin
156
+ @Test
157
+ fun `loads user from real db`() = runTest {
158
+ Testcontainers.start("postgres:16")
159
+ val repo = JdbcUserRepository(dataSource)
160
+ repo.save(User("u1", "a@x.com"))
161
+ assertEquals("a@x.com", repo.find("u1")?.email)
162
+ }
163
+ ```
164
+
165
+ Keep integration tests in a separate source set (`src/integrationTest/`) with its own Gradle task — `./gradlew integrationTest`.
166
+
167
+ ## Android UI tests
168
+
169
+ - **Jetpack Compose**: `createComposeRule()`, `onNodeWithText(...).performClick()`.
170
+ - **Views**: Espresso.
171
+
172
+ ```kotlin
173
+ @get:Rule
174
+ val compose = createComposeRule()
175
+
176
+ @Test
177
+ fun `shows user name`() {
178
+ compose.setContent { UserCard(user = sampleUser) }
179
+ compose.onNodeWithText("Alice").assertIsDisplayed()
180
+ }
181
+ ```
182
+
183
+ ## Code coverage
184
+
185
+ JaCoCo for JVM / Android. See `coding-standards/references/tdd.md` for target numbers.
186
+
187
+ ```kotlin
188
+ // build.gradle.kts
189
+ plugins { jacoco }
190
+ tasks.test { finalizedBy(tasks.jacocoTestReport) }
191
+ ```
192
+
193
+ ## Property tests — Kotest property API
194
+
195
+ ```kotlin
196
+ import io.kotest.property.*
197
+ import io.kotest.property.arbitrary.*
198
+
199
+ "sort is idempotent" - {
200
+ checkAll(Arb.list(Arb.int())) { xs ->
201
+ xs.sorted() shouldBe xs.sorted().sorted()
202
+ }
203
+ }
204
+ ```
205
+
206
+ Great for parsers, serializers, invariants.
207
+
208
+ ## Anti-patterns
209
+
210
+ | Anti-pattern | Fix |
211
+ |---|---|
212
+ | `Thread.sleep` in coroutine tests | `delay` + `runTest` |
213
+ | `runBlocking` in tests of suspend functions | `runTest` virtualizes time |
214
+ | Over-mocking (10 mocks for 20 LOC) | Use a fake |
215
+ | `verify { ... }` counts that duplicate the mock setup | Assert observable behaviour instead |
216
+ | Snapshot tests for flaky output (time, UUID) | Inject fakes / fixed values |
217
+ | Tests that depend on coroutine ordering | Use `runTest` scheduler primitives, not real threads |
218
+ | Real network in unit tests | Fake the HTTP client or use `MockWebServer` |
219
+ | Test name like `test1`, `test2` | Describe behaviour: \`\`add combines positives\`\` |
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: mobile
3
+ description: Mobile end skill — architecture, offline-first, state, performance, lifecycle. Platform-neutral (iOS / Android / cross-platform). Pair with a language skill (`swift` / `kotlin` / `typescript`) and `coding-standards`.
4
+ origin: original
5
+ ---
6
+
7
+ # Mobile
8
+
9
+ Native and cross-platform mobile app architecture. **Platform-neutral**; load with a language skill for syntax and a framework perspective.
10
+
11
+ ## When to load
12
+
13
+ - Native iOS (Swift + UIKit / SwiftUI) or Android (Kotlin + Jetpack Compose / Views)
14
+ - Cross-platform: React Native, Flutter, Kotlin Multiplatform, .NET MAUI
15
+ - Hybrid: Capacitor, Cordova
16
+ - Topics: lifecycle, offline, state, navigation, platform APIs, app size, battery
17
+
18
+ ## Core principles
19
+
20
+ 1. **Lifecycle is the tax.** Your app gets suspended, killed, restored — design for it, don't pretend it's a desktop.
21
+ 2. **Offline is the default.** The network is bad, intermittent, or absent. Degrade gracefully.
22
+ 3. **Screens are ephemeral; state persists.** Recreate UI from state on re-entry.
23
+ 4. **One source of truth per piece of data.** Caches reconcile with server truth; UIs reflect the cache.
24
+ 5. **Trim aggressively.** Every 1 MB costs downloads and disk. Every dep adds attack surface and supply risk.
25
+ 6. **Respect the user's battery and data.** Background work is a privilege; schedule wisely.
26
+ 7. **Accessibility is platform-native.** Use TalkBack / VoiceOver primitives, not custom read-aloud.
27
+ 8. **Test on a low-end device with slow network.** "Works on latest iPhone" is not a release signal.
28
+
29
+ ## How to use references
30
+
31
+ | Reference | When to load |
32
+ |---|---|
33
+ | [`references/architecture.md`](references/architecture.md) | MVVM / MVI / TCA / clean architecture, DI, module boundaries |
34
+ | [`references/state-and-data.md`](references/state-and-data.md) | Local store, sync, offline-first, optimistic updates, reactive streams |
35
+ | [`references/navigation.md`](references/navigation.md) | Stack, tab, modal, deep links, universal links, back-stack |
36
+ | [`references/performance.md`](references/performance.md) | Startup, frame rate, memory, battery, app size |
37
+ | [`references/platform.md`](references/platform.md) | Permissions, notifications, background tasks, biometrics, crypto / keychain |
38
+
39
+ ## Forbidden patterns (auto-reject)
40
+
41
+ - Blocking the UI thread (network, disk, JSON parse) in the request path
42
+ - Storing secrets / tokens in `UserDefaults` / `SharedPreferences` — use Keychain / Keystore
43
+ - `Thread.sleep` / `Thread.sleep(...)` in UI code
44
+ - Manual lifecycle handling in modern frameworks (use `ViewModel` / `StateObject` / scoped observers)
45
+ - Global mutable singletons for business data
46
+ - Hard-coded base URLs in release builds
47
+ - Silent swallow of network errors (no indicator, no retry)
48
+ - Unbounded in-memory caches (OOM kills)
49
+ - Hitting `/me` on every screen (fetch once, cache, invalidate on write)
50
+ - "Reload the world" after any change (no delta / optimistic update)
@@ -0,0 +1,204 @@
1
+ # Mobile — Architecture
2
+
3
+ MVVM, MVI, TCA, clean architecture. Platform-neutral.
4
+
5
+ ## The shapes
6
+
7
+ | Pattern | Idea | Good for |
8
+ |---|---|---|
9
+ | **MVVM** | View ← binds → ViewModel → Model | SwiftUI, Jetpack Compose, most modern apps |
10
+ | **MVI** | View → Intent → Reducer → State → View | Deterministic, testable flows; complex screens |
11
+ | **TCA** (iOS) | Redux-like, dependency-injected | Teams that want explicit wiring everywhere |
12
+ | **Clean / Onion** | Layers; domain in the middle | Large apps with multiple teams |
13
+
14
+ Most apps today land on MVVM + unidirectional data flow. MVI is MVVM with explicit state / intent naming. Pick one per project.
15
+
16
+ ## Layering (clean-ish)
17
+
18
+ ```
19
+ ┌──────────────────────────────────────────┐
20
+ │ UI (screens, components) │ — platform-specific
21
+ ├──────────────────────────────────────────┤
22
+ │ Presentation (ViewModels / Stores) │ — state + actions
23
+ ├──────────────────────────────────────────┤
24
+ │ Domain (entities, use cases) │ — pure, platform-free
25
+ ├──────────────────────────────────────────┤
26
+ │ Data (repositories + sources: API, DB) │ — infrastructure
27
+ └──────────────────────────────────────────┘
28
+ ```
29
+
30
+ - **UI** calls into the ViewModel; ViewModel calls into use cases / repos.
31
+ - **Domain** has no Android / iOS imports. Shareable across platforms.
32
+ - **Data** implements the interfaces the Domain declares.
33
+
34
+ Like `backend/layering.md`: dependency flows inward. Don't let `Context` / `UIApplication` leak into domain code.
35
+
36
+ ## ViewModel — owner of screen state
37
+
38
+ ```
39
+ ViewModel:
40
+ state: observable<ScreenState>
41
+
42
+ onIntent(Intent):
43
+ match intent:
44
+ case .load: state = .loading; launch { fetchUser() }
45
+ case .retry: state = .loading; launch { fetchUser() }
46
+ case .select(id): navigator.push(Detail(id))
47
+ ```
48
+
49
+ Responsibilities:
50
+ - Own the screen's state
51
+ - Talk to use cases / repos
52
+ - Translate user input into state changes
53
+ - Survive configuration changes (rotation, dark mode) — framework handles if you use the right scope
54
+
55
+ NOT responsibilities:
56
+ - Drawing pixels
57
+ - Navigating directly (use a navigator / router)
58
+ - Accessing platform APIs directly (get them via DI)
59
+
60
+ ## Unidirectional data flow
61
+
62
+ ```
63
+ User action ────▶ Intent ────▶ ViewModel ────▶ State
64
+ ▲ │
65
+ └────────────────────────────────────────┘
66
+ View observes
67
+ ```
68
+
69
+ - Views are a function of state. Don't mutate state inside views.
70
+ - Intents are explicit. Don't let the view call arbitrary business logic.
71
+ - State changes are traceable (log the intent) for debugging.
72
+
73
+ ## Dependency injection
74
+
75
+ Every non-trivial app uses DI. Options vary by platform:
76
+
77
+ | Platform | Common choices |
78
+ |---|---|
79
+ | Android | Hilt, Koin, manual constructor injection |
80
+ | iOS | Swinject, @Environment, manual |
81
+ | Cross-platform (KMP / RN) | Manual / small DI libs |
82
+
83
+ Prefer constructor injection; simpler, testable.
84
+
85
+ Anti-pattern: service locator / global accessor. They hide deps and make testing painful.
86
+
87
+ ## Modularization
88
+
89
+ As the app grows, split by feature (not by layer):
90
+
91
+ ```
92
+ app/
93
+ ├── feature-auth/
94
+ │ ├── domain/ presentation/ data/
95
+ ├── feature-orders/
96
+ │ ├── domain/ presentation/ data/
97
+ ├── feature-profile/
98
+ ├── shared/
99
+ │ ├── ui/ network/ persistence/
100
+ └── app-shell/ # wires features together, navigation root
101
+ ```
102
+
103
+ Benefits:
104
+ - Parallel team work.
105
+ - Faster incremental builds.
106
+ - Forces clean interfaces between features.
107
+
108
+ Over-modularization before the app needs it is friction. Start single-module; split when team size or build time hurts.
109
+
110
+ ## Use cases
111
+
112
+ A use case is one screen's intent: `GetUserProfile`, `PlaceOrder`, `LogOut`.
113
+
114
+ ```
115
+ class PlaceOrder(
116
+ val orderRepo: OrderRepository,
117
+ val paymentGateway: PaymentGateway,
118
+ val analytics: Analytics,
119
+ ) {
120
+ suspend fun invoke(cmd: PlaceOrderCommand): Result<OrderId> {
121
+ val order = Order.create(cmd) // domain rule
122
+ orderRepo.save(order)
123
+ paymentGateway.authorize(order.total)
124
+ analytics.track("order_placed", order.id)
125
+ return Result.success(order.id)
126
+ }
127
+ }
128
+ ```
129
+
130
+ Small, one method, testable. Don't force every screen to go through a use case — if it's just loading data, call the repo from the ViewModel.
131
+
132
+ ## Repository — hides the data sources
133
+
134
+ ```
135
+ interface UserRepository {
136
+ suspend fun get(id: String): User // hits cache, falls back to API
137
+ suspend fun refresh(id: String): User // forces network
138
+ fun observe(id: String): Flow<User> // reactive stream
139
+ }
140
+ ```
141
+
142
+ Inside:
143
+ - Local store (Room / SwiftData / Realm / SQLite).
144
+ - Remote source (HTTP client).
145
+ - Reconciliation logic (cache + network).
146
+
147
+ Consumers don't know or care. That's the point.
148
+
149
+ ## Error surfacing
150
+
151
+ Domain errors → enum / sealed type that the ViewModel maps to UI state.
152
+
153
+ ```
154
+ sealed class OrderError {
155
+ object InsufficientFunds : OrderError()
156
+ object OutOfStock : OrderError()
157
+ data class Network(val cause: Throwable) : OrderError()
158
+ data class Unknown(val cause: Throwable) : OrderError()
159
+ }
160
+
161
+ state = State.Error(OrderError.InsufficientFunds)
162
+ // UI: show a dialog with specific copy + action
163
+ ```
164
+
165
+ Never show a raw exception message to the user. Map to something actionable.
166
+
167
+ ## Coroutine / async scoping
168
+
169
+ - Android: `viewModelScope`, `lifecycleScope`.
170
+ - iOS Swift: `Task`, tied to view lifecycle or `@StateObject`.
171
+ - React Native: effects tied to component lifecycle; clean up on unmount.
172
+ - Flutter: `dispose()` on `StatefulWidget`; `Stream` subscriptions cancelled.
173
+
174
+ Don't start work that outlives the scope. Leaked background tasks cause crashes, stale state, wasted battery.
175
+
176
+ ## Feature flags
177
+
178
+ Every mobile release is "all or nothing" for some hours. Feature flags let you ship code dark and flip when ready.
179
+
180
+ - Client flag service (LaunchDarkly, ConfigCat, home-grown).
181
+ - Defaults that are safe if the service is unreachable (offline start).
182
+ - Short-lived; clean up flags once fully rolled out.
183
+
184
+ ## Telemetry hooks
185
+
186
+ - App open, session start/end
187
+ - Screen viewed (one event per meaningful screen; don't inflate)
188
+ - Intent triggered (for key flows — signup, checkout)
189
+ - Error happened (with anonymous context; never PII)
190
+
191
+ Ship to a pipeline (Firebase Analytics, Segment, MixPanel, self-hosted) with a schema you control.
192
+
193
+ ## Anti-patterns
194
+
195
+ | Anti-pattern | Fix |
196
+ |---|---|
197
+ | Business logic in view files | Move to ViewModel / use case |
198
+ | ViewModel holds a `Context` / `UIViewController` reference | Leaks UI into presentation; refactor |
199
+ | Singleton "service" with mutable business data | DI via constructor; scope to a request or screen |
200
+ | Navigation logic scattered across screens | Navigator / router at app level |
201
+ | Each screen fetches `/me` on start | Cache; fetch once, invalidate on logout / update |
202
+ | Data layer throws raw SQL exceptions to the UI | Repository maps to domain errors |
203
+ | Feature modules depending on each other arbitrarily | Feature modules depend on `shared/`; never on siblings |
204
+ | "Reactive Everywhere" — observable every primitive | Use streams for state that changes; keep static state static |
@@ -0,0 +1,158 @@
1
+ # Mobile — Navigation
2
+
3
+ Stack, tabs, modals, deep links, universal / app links.
4
+
5
+ ## Core model
6
+
7
+ Mobile navigation is a stack (usually multiple, one per tab). The user always has:
8
+ - A clear back action (system or in-app).
9
+ - An understanding of where they are (title, breadcrumb, tab highlight).
10
+ - A way to exit / cancel a flow.
11
+
12
+ ## Stack
13
+
14
+ ```
15
+ Root ─▶ List ─▶ Detail ─▶ Edit ─▶ Confirmation
16
+
17
+ └── back returns here, not to List
18
+ ```
19
+
20
+ Rules:
21
+ - **Keep stacks shallow.** 4+ levels usually means you should bring the user back to a root or use a modal.
22
+ - **Clear the stack after destructive flows** (signup complete, logout, delete-account).
23
+ - **Don't insert navigation into the middle** (teleports jarring to users).
24
+
25
+ ## Tabs
26
+
27
+ Top-level categories — don't abuse. Rule of thumb: 3–5 tabs max.
28
+
29
+ - Each tab has its own stack.
30
+ - Switching tabs preserves per-tab state (position in list, scroll).
31
+ - Re-selecting the active tab scrolls to top (standard iOS / Android behavior).
32
+
33
+ ## Modals
34
+
35
+ Use for tasks that are:
36
+ - Focused (one clear goal).
37
+ - Interruptible (user can cancel).
38
+ - Temporary (they don't want to return here later).
39
+
40
+ ```
41
+ [ Close ] Edit profile [ Save ]
42
+ ```
43
+
44
+ Don't use modals for content the user needs to return to. A profile page isn't a modal; editing the profile is.
45
+
46
+ Bottom sheets / partial modals are modals too. Same rules.
47
+
48
+ ## Deep links
49
+
50
+ Every route should be addressable by URL.
51
+
52
+ ```
53
+ myapp://orders/123
54
+ https://example.com/orders/123
55
+ ```
56
+
57
+ - **Custom scheme** (`myapp://`) — older; still used for some flows.
58
+ - **Universal links (iOS) / App links (Android)** — modern; open the app if installed, fall back to web otherwise. Required for secure flows.
59
+
60
+ Setup is fiddly:
61
+ - iOS: `apple-app-site-association` file, `associatedDomains` entitlement.
62
+ - Android: intent filters + Digital Asset Links.
63
+
64
+ Test with scanner tools (`branch.io` validator, `assetlinks.google.com/tools`).
65
+
66
+ ## Back-stack hydration
67
+
68
+ When the user lands via a deep link, the back stack should reflect the path they'd have if they navigated manually.
69
+
70
+ ```
71
+ Link: /orders/123/items/7
72
+
73
+ # ❌ back pops out of the app
74
+ [ items/7 ]
75
+
76
+ # ✅ back goes to the logical parent
77
+ [ root ] ─▶ [ orders ] ─▶ [ order 123 ] ─▶ [ item 7 ]
78
+ ```
79
+
80
+ Build the stack when resolving the link; don't leave the user stuck.
81
+
82
+ ## Navigation state is your state
83
+
84
+ Declarative navigation (Compose Navigation, SwiftUI `NavigationStack`, React Navigation) treats the route as a data structure. Restore on cold start by persisting and rehydrating.
85
+
86
+ ```
87
+ state = [ Home, Orders, Order(id=123) ]
88
+ // on process death, serialize; on cold start, rehydrate
89
+ ```
90
+
91
+ Imperative navigation (push/pop calls) is harder to persist. Prefer declarative in new apps.
92
+
93
+ ## Passing data between screens
94
+
95
+ Options, in order of preference:
96
+
97
+ 1. **Route params** (IDs only). `order/123`. Serializable, deep-linkable.
98
+ 2. **Shared ViewModel / store**. The source of truth lives in one place.
99
+ 3. **Nav callbacks** (for single-screen outcomes: "picker returns a country"). Keep narrow.
100
+ 4. **Serialized object in the route** — last resort; breaks if the model changes.
101
+
102
+ Don't pass live objects / closures across screens; the framework can't restore them after process death.
103
+
104
+ ## Tabs + deep links
105
+
106
+ Deep link lands on a tab and a stack within that tab:
107
+
108
+ ```
109
+ /profile/settings ─▶ tab=Profile, stack=[Profile, Settings]
110
+ /cart ─▶ tab=Cart, stack=[Cart]
111
+ ```
112
+
113
+ Set the tab, then restore the stack, in one atomic transition.
114
+
115
+ ## Transitions
116
+
117
+ - **Native** where available (push / pop, modal present). Users recognize the motion.
118
+ - **Custom** only for brand moments. Custom transitions are slow to build and often feel off on other devices.
119
+ - Respect `prefers-reduced-motion` — reduce or skip animations.
120
+
121
+ ## Bottom navigation vs. side drawer
122
+
123
+ - **Bottom nav** (mobile-native, iOS tabs / Android BottomNavigationView): 3–5 top-level items, always visible.
124
+ - **Side drawer** (hamburger): de-emphasized items, usable with one hand awkward, often hurts discoverability.
125
+
126
+ Bottom nav by default. Drawer for overflow or very large navigation sets.
127
+
128
+ ## Back button (Android) and gestures
129
+
130
+ - **Android hardware/system back**: must work everywhere. Never block.
131
+ - **iOS swipe-back**: enabled by default on navigation stacks; don't disable without reason.
132
+ - **Android predictive back** (13+): opt in via `BackInvokedCallback` to show a preview.
133
+
134
+ Handle in-flow back (dismiss modal, close keyboard, confirm-before-exit for unsaved data).
135
+
136
+ ## Sign-out / session expiry
137
+
138
+ On logout or token revocation:
139
+ 1. Clear secure storage (tokens, keys, cached user).
140
+ 2. Wipe in-memory state.
141
+ 3. Reset navigation to the auth / onboarding root.
142
+ 4. Cancel any in-flight requests / subscriptions.
143
+
144
+ Skipping step 3 leaves the user on a logged-in screen with no data — confusing.
145
+
146
+ ## Anti-patterns
147
+
148
+ | Anti-pattern | Fix |
149
+ |---|---|
150
+ | Deep link lands on a screen with no back stack | Hydrate the full path |
151
+ | Complex data serialized into the URL | Pass an ID; fetch from source |
152
+ | 8-tab bottom nav | Consolidate; use "More" or search |
153
+ | Modal pushing another modal pushing another | Rethink the flow |
154
+ | Blocking system back | Always respond; ask if truly destructive |
155
+ | Navigation in `useEffect` during render | Navigate on user action or in route loader |
156
+ | Different bottom nav per screen | Confusing; top-level navigation should be stable |
157
+ | Losing form state on rotation / background | Persist draft to local store |
158
+ | "Back" that goes forward (resets to root) | Match user expectation of "previous" |