Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:22 +08:00
commit 2c8fd6d9a0
22 changed files with 8353 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "py-plugin",
"description": "A plugin stores all skills for py projects",
"version": "1.0.14",
"author": {
"name": "dai.pham"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# py-plugin
A plugin stores all skills for py projects

117
plugin.lock.json Normal file
View File

@@ -0,0 +1,117 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:daispacy/py-claude-marketplace:py-plugin",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b7986a7d679ee30ced2536138ac6e1e849ef644f",
"treeHash": "472ad6a3155f907930352d03b17d5d4ad84f722a42302f2609428c375f3b338a",
"generatedAt": "2025-11-28T10:16:00.637217Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "py-plugin",
"description": "A plugin stores all skills for py projects",
"version": "1.0.14"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "0163fee5cabf53d98222f09ef07dd5414019adb7c79b95275f3a5922b99b0111"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "1db18d788dca8a6e9e84aa69f895574fd59f478002328290c6394e22a958a04e"
},
{
"path": "skills/clean-architecture-review/examples.md",
"sha256": "fdc1e9674c26bf97deb7fc3de4556c811ac18632f42bf8e5c178749a135114ac"
},
{
"path": "skills/clean-architecture-review/SKILL.md",
"sha256": "edd7a0d4c7e77ea1233f5344e3a2ab384f00ea2372b2f7c534de601d1adeb89b"
},
{
"path": "skills/ios-naming-conventions/examples.md",
"sha256": "f3d2203266dcc77a6d8f5ffb2ffd3d87de664d755fc815d5d11d423017bb2395"
},
{
"path": "skills/ios-naming-conventions/SKILL.md",
"sha256": "4a707fe4830fe2d5555055280839aef492036a3fdb9e73bb942f7c1fbe6b2e7f"
},
{
"path": "skills/skill-builder/SKILL.md",
"sha256": "00ec0273c1a3300dfe5a185567927925b3b1d01c257a4a52eebacc43d23b20b1"
},
{
"path": "skills/copilot-agent-builder/examples.md",
"sha256": "b70b2bf1ef1abcb807de19b0a8884aa85ebc79db218af0253bd4364169b6f382"
},
{
"path": "skills/copilot-agent-builder/templates.md",
"sha256": "38b930e14ee39e0cb32ba8af3856c915985daca7cc440f0ae6a98d0d60af0fb5"
},
{
"path": "skills/copilot-agent-builder/SKILL.md",
"sha256": "710af310a1ec012ba124e223257a8eaa4ec33fc639e172844096f5c1f6997c70"
},
{
"path": "skills/instruments/examples.md",
"sha256": "6492d3764fafde1d7babf13d73be4757e386cb1b226fb425eb0cff08dc71c7c3"
},
{
"path": "skills/instruments/SKILL.md",
"sha256": "38f22b0a9544c22eb6259a01ce4fc4908949bcd6b5351d19f5b74a9f9736bbcf"
},
{
"path": "skills/android-code-review/examples.md",
"sha256": "a41c1dd2af1a2e19d93c7bffc1dfad2cd21a3c3b3f2097ce93bf0ebc102cfe02"
},
{
"path": "skills/android-code-review/SKILL.md",
"sha256": "f5b52e305445979c9eef35a756fc7c4bc9bad4a43bf082349c40247759fede2f"
},
{
"path": "skills/android-code-review/standards.md",
"sha256": "4cc63abd13cdf67f8d89cfc322990bfaa343a0367e922c9908c7e20bf2adf002"
},
{
"path": "skills/ios-code-review/examples.md",
"sha256": "5fc609265eb9a87ecd37b76eaef5e5b285032992d48e2218db0ab1017f16504a"
},
{
"path": "skills/ios-code-review/SKILL.md",
"sha256": "da4d171fb45a250e7806247c063b3cd337d2ad940260c1e864fdd1db538e4364"
},
{
"path": "skills/rxswift-memory-check/examples.md",
"sha256": "ff478403c43b2fa1ebc61fc3bdc3a0709498baf1518e27361140b4b4df6b9bd0"
},
{
"path": "skills/rxswift-memory-check/SKILL.md",
"sha256": "d522e2954a2b99f59f9c9846c57f04b7180b8f2f1e97ef0fbb91a9084d9d68f3"
},
{
"path": "skills/mcp-tool-generator/examples.md",
"sha256": "102c2691dd93bbbd923d702f5fe2317a4bd4eb9e1e9b524c8dedae8b470b1a3b"
},
{
"path": "skills/mcp-tool-generator/SKILL.md",
"sha256": "34ae905365a1c23fcc79d50248875efb23de0334be02a263d3c18fe4d5627fc0"
}
],
"dirSha256": "472ad6a3155f907930352d03b17d5d4ad84f722a42302f2609428c375f3b338a"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,180 @@
---
name: android-code-review
description: Comprehensive Android Kotlin code review for Payoo Android app. Checks Kotlin best practices, coroutines, Clean Architecture, MVVM patterns, lifecycle management, dependency injection, memory management, security, and performance. Use when reviewing Kotlin files, pull requests, or ViewModels, Activities, Fragments, UseCases, and Repositories.
allowed-tools: Read, Grep, Glob
---
# Android Code Review
Expert Android code reviewer for Payoo Android application, specializing in Kotlin, Coroutines, Jetpack components, and Clean Architecture patterns.
## When to Activate
- "review android code", "check android file", "review android PR"
- Mentions Kotlin/Java files: Activity, Fragment, ViewModel, UseCase, Repository
- "code quality", "best practices", "check android standards"
- Coroutines, Clean Architecture, MVVM patterns, Jetpack components
## Review Process
### Step 1: Identify Scope
Determine what to review:
- Specific files (e.g., "PaymentViewModel.kt")
- Directories (e.g., "payment module")
- Git changes (recent commits, PR diff)
- Entire module or feature
### Step 2: Read and Analyze
Use Read tool to examine files, checking against 7 core categories.
### Step 3: Apply Standards
#### 1. Naming Conventions ✅
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_RETRY_COUNT`)
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
- **No abbreviations** except URL, ID, API, HTTP, UI
- **Views**: Include type suffix (e.g., `amountEditText`, `submitButton`)
#### 2. Kotlin Best Practices 🎯
- **Null Safety**: Use `?` and `!!` appropriately, prefer safe calls
- **Data Classes**: Use for DTOs and models
- **Sealed Classes**: For state management and result types
- **Extension Functions**: For reusable utilities
- **Scope Functions**: Use `let`, `apply`, `run`, `also`, `with` correctly
- **Immutability**: Prefer `val` over `var`
#### 3. Coroutines Patterns 🔄
- **Scope**: Use `viewModelScope`, `lifecycleScope` appropriately
- **Dispatchers**: `Dispatchers.IO` for network/DB, `Dispatchers.Main` for UI
- **Cancellation**: All coroutines properly cancelled
- **Error Handling**: Use `try-catch` or `runCatching` in coroutines
- **Flows**: Use `StateFlow`, `SharedFlow` for reactive data
- **No blocking**: Never use `runBlocking` in production code
#### 4. Clean Architecture 🏗️
- **Flow**: ViewModel → UseCase → Repository → DataSource (API/DB)
- **ViewModels**: Extend `ViewModel`, expose UI state via `StateFlow`
- **UseCases**: Contain business logic, single responsibility
- **Repositories**: Abstract data sources
- **DI**: Dependencies injected (Dagger/Hilt/Koin)
- **Layers**: Strict separation (Presentation/Domain/Data)
#### 5. Lifecycle Management 🔁
- **ViewModels**: Don't hold Activity/Fragment references
- **Observers**: Use `viewLifecycleOwner` in Fragments
- **Coroutines**: Launch in lifecycle-aware scopes
- **Resources**: Clean up in `onDestroy` or `onCleared`
- **Configuration Changes**: Handle properly
#### 6. Security 🔒
- **Sensitive Data**: Use EncryptedSharedPreferences
- **API Keys**: Never hardcode, use BuildConfig
- **Network**: HTTPS only, certificate pinning if needed
- **Logs**: No sensitive data in production logs
- **Input Validation**: Sanitize user inputs
- **ProGuard/R8**: Proper obfuscation rules
#### 7. Performance ⚡
- **Background Work**: Network and DB on IO dispatcher
- **Memory Leaks**: No Activity/Context leaks
- **RecyclerView**: Use DiffUtil, ViewBinding
- **Images**: Use Coil/Glide with proper caching
- **Database**: Room queries optimized with indexes
- **Lazy Loading**: Load data on demand
### Step 4: Generate Report
Provide structured output with:
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
- **Issues by category**: Organized findings
- **Code examples**: Current vs. fixed code
- **Explanations**: Why it matters
- **Recommendations**: Prioritized actions
## Severity Levels
🔴 **Critical** - Fix immediately
- Memory leaks (Activity/Context references)
- Coroutines not cancelled → Resource leak
- Sensitive data in plain SharedPreferences
- UI updates on background thread → Crash risk
- Hardcoded API keys or secrets
🟠 **High Priority** - Fix soon
- Missing error handling in coroutines
- Wrong Dispatcher usage
- ViewModel calling repository directly (skip UseCase)
- Business logic in ViewModel/Activity
- No ProGuard rules for critical code
🟡 **Medium Priority** - Should improve
- Not using lifecycle-aware components
- Poor naming conventions
- Not using data classes for models
- Missing null safety checks
- Inefficient RecyclerView usage
🟢 **Low Priority** - Nice to have
- Inconsistent code style
- Could use more extension functions
- Documentation improvements
## Output Format
```markdown
# Android Code Review Report
## Summary
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
- By category: Naming: X, Kotlin: X, Coroutines: X, Architecture: X, etc.
## Critical Issues
### 🔴 [Category] - [Issue Title]
**File**: `path/to/file.kt:line`
**Current**:
```kotlin
// problematic code
```
**Fix**:
```kotlin
// corrected code
```
**Why**: [Explanation of impact]
---
## Recommendations
1. Fix all critical issues immediately
2. Address high priority before next release
3. Plan medium priority for next sprint
## Positive Observations
✅ [Acknowledge well-written code]
```
## Quick Reference
**Standards**: See `standards.md` in this skill directory for detailed Android coding standards including:
- Kotlin best practices and idioms
- Coroutines patterns and anti-patterns
- Clean Architecture implementation
- Dependency injection patterns
- Security guidelines
- Performance optimization techniques
**Examples**: See `examples.md` for extensive code examples, common issues, and recommended patterns.
## Tips
- **Be thorough**: Check all 7 categories
- **Be specific**: Reference exact line numbers
- **Be constructive**: Explain why, not just what
- **Be practical**: Prioritize by severity
- **Be encouraging**: Acknowledge good code
- **Context matters**: Consider app-specific requirements

View File

@@ -0,0 +1,845 @@
# Android Code Review Examples
Real-world examples of common issues and recommended patterns for Android Kotlin development.
## Example 1: Memory Leak - Activity Reference in ViewModel
### ❌ BAD: Holding Activity Reference
```kotlin
// PaymentViewModel.kt
class PaymentViewModel(
private val activity: PaymentActivity // Memory leak!
) : ViewModel() {
fun onPaymentComplete() {
activity.showSuccessDialog() // Crash if Activity destroyed!
activity.finish()
}
}
// PaymentActivity.kt
class PaymentActivity : AppCompatActivity() {
private val viewModel by viewModels<PaymentViewModel> {
PaymentViewModelFactory(this) // Passing Activity reference!
}
}
```
**Issues:**
- 🔴 **Critical**: Memory leak - ViewModel outlives Activity
- 🔴 **Critical**: Crash risk if Activity destroyed but ViewModel still active
- 🟠 **High**: Tight coupling between ViewModel and View
### ✅ GOOD: Event-Based Navigation
```kotlin
// PaymentViewModel.kt
class PaymentViewModel : ViewModel() {
private val _events = MutableSharedFlow<PaymentEvent>()
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
fun onPaymentComplete() {
viewModelScope.launch {
_events.emit(PaymentEvent.ShowSuccessDialog)
_events.emit(PaymentEvent.Finish)
}
}
}
sealed class PaymentEvent {
object ShowSuccessDialog : PaymentEvent()
object Finish : PaymentEvent()
data class NavigateTo(val destination: String) : PaymentEvent()
}
// PaymentActivity.kt
@AndroidEntryPoint
class PaymentActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: PaymentViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
PaymentEvent.ShowSuccessDialog -> showSuccessDialog()
PaymentEvent.Finish -> finish()
is PaymentEvent.NavigateTo -> navigate(event.destination)
}
}
}
}
}
```
**Benefits:**
- ✅ No memory leaks - ViewModel doesn't hold Activity reference
- ✅ Lifecycle-safe - Events only processed when Activity is active
- ✅ Testable - ViewModel logic can be tested independently
---
## Example 2: Coroutine Scope and Cancellation
### ❌ BAD: GlobalScope and No Cancellation
```kotlin
class PaymentViewModel : ViewModel() {
fun loadPayments() {
GlobalScope.launch { // Never cancelled!
val payments = repository.getPayments()
_payments.value = payments // Can crash if ViewModel cleared
}
}
fun processPayment(request: PaymentRequest) {
GlobalScope.launch(Dispatchers.Main) {
val result = repository.processPayment(request) // Blocks UI thread!
_result.value = result
}
}
}
```
**Issues:**
- 🔴 **Critical**: GlobalScope coroutines never cancelled - resource leak
- 🔴 **Critical**: Network/DB on Main thread - ANR risk
- 🟠 **High**: Crash risk when updating UI after ViewModel cleared
### ✅ GOOD: ViewModelScope and Proper Dispatchers
```kotlin
class PaymentViewModel(
private val repository: PaymentRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Payment>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Payment>>> = _uiState.asStateFlow()
fun loadPayments() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
// Repository handles IO dispatcher internally
val payments = repository.getPayments()
_uiState.value = UiState.Success(payments)
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error: ${e.message}")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
}
}
}
fun processPayment(request: PaymentRequest) {
viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching {
repository.processPayment(request)
}.onSuccess { result ->
_uiState.value = UiState.Success(result)
}.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
}
}
// Repository with proper dispatcher
class PaymentRepositoryImpl(
private val api: PaymentApi,
private val dao: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
api.fetchPayments()
}
override suspend fun processPayment(request: PaymentRequest): PaymentResult =
withContext(ioDispatcher) {
api.processPayment(request)
}
}
```
**Benefits:**
- ✅ Automatic cancellation when ViewModel cleared
- ✅ Proper thread management - IO work on background
- ✅ Comprehensive error handling
- ✅ Single source of truth for UI state
---
## Example 3: Clean Architecture Violation
### ❌ BAD: ViewModel with Business Logic and Direct API Calls
```kotlin
class PaymentViewModel(
private val api: PaymentApi // Wrong layer!
) : ViewModel() {
private val _result = MutableLiveData<PaymentResult>()
val result: LiveData<PaymentResult> = _result
fun processPayment(amount: String, merchantId: String) {
viewModelScope.launch {
// Business logic in ViewModel - WRONG!
val amountBigDecimal = try {
BigDecimal(amount)
} catch (e: NumberFormatException) {
_result.value = PaymentResult.Error("Invalid amount")
return@launch
}
if (amountBigDecimal < BigDecimal("1000")) {
_result.value = PaymentResult.Error("Minimum amount is 1000 VND")
return@launch
}
val fee = amountBigDecimal * BigDecimal("0.02") // Business logic!
val total = amountBigDecimal + fee
// Calling API directly - WRONG!
try {
val response = api.processPayment(
PaymentRequest(
amount = total,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
)
_result.value = PaymentResult.Success(response)
} catch (e: Exception) {
_result.value = PaymentResult.Error(e.message ?: "Error")
}
}
}
}
```
**Issues:**
- 🟠 **High**: Business logic in ViewModel (validation, fee calculation)
- 🟠 **High**: ViewModel depends on Data layer (API) directly
- 🟡 **Medium**: No UseCase - business logic not reusable
- 🟡 **Medium**: Mixing concerns - validation, calculation, networking
### ✅ GOOD: Proper Clean Architecture Layers
```kotlin
// Domain Layer - UseCase
class ProcessPaymentUseCase(
private val repository: PaymentRepository,
private val validator: PaymentValidator,
private val feeCalculator: FeeCalculator
) {
suspend operator fun invoke(
amount: String,
merchantId: String
): Result<PaymentResult> = runCatching {
// Business logic in UseCase
val amountBigDecimal = validator.parseAndValidateAmount(amount)
validator.validateMerchant(merchantId)
val fee = feeCalculator.calculateFee(amountBigDecimal)
val total = amountBigDecimal + fee
val request = PaymentRequest(
amount = total,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
repository.processPayment(request)
}
}
// Domain Layer - Validators and Calculators
class PaymentValidator {
fun parseAndValidateAmount(amount: String): BigDecimal {
val amountBigDecimal = amount.toBigDecimalOrNull()
?: throw IllegalArgumentException("Invalid amount format")
if (amountBigDecimal < MIN_AMOUNT) {
throw IllegalArgumentException("Minimum amount is $MIN_AMOUNT VND")
}
return amountBigDecimal
}
fun validateMerchant(merchantId: String) {
if (merchantId.isBlank()) {
throw IllegalArgumentException("Invalid merchant ID")
}
}
companion object {
private val MIN_AMOUNT = BigDecimal("1000")
}
}
class FeeCalculator {
fun calculateFee(amount: BigDecimal): BigDecimal {
return amount * FEE_RATE
}
companion object {
private val FEE_RATE = BigDecimal("0.02")
}
}
// Presentation Layer - ViewModel
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
fun processPayment(amount: String, merchantId: String) {
viewModelScope.launch {
_uiState.value = PaymentUiState.Loading
processPaymentUseCase(amount, merchantId)
.onSuccess { result ->
_uiState.value = PaymentUiState.Success(result)
}
.onFailure { error ->
_uiState.value = PaymentUiState.Error(error.message ?: "Unknown error")
}
}
}
}
// Data Layer - Repository
class PaymentRepositoryImpl(
private val remoteDataSource: PaymentRemoteDataSource,
private val localDataSource: PaymentLocalDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun processPayment(
request: PaymentRequest
): PaymentResult = withContext(ioDispatcher) {
val result = remoteDataSource.processPayment(request)
localDataSource.savePayment(result.toEntity())
result
}
}
```
**Benefits:**
- ✅ Clear separation of concerns
- ✅ Business logic in Domain layer (UseCase)
- ✅ ViewModel only handles presentation logic
- ✅ Reusable business logic
- ✅ Testable - each layer can be tested independently
---
## Example 4: Insecure Storage
### ❌ BAD: Plain SharedPreferences for Sensitive Data
```kotlin
class AuthManager(private val context: Context) {
private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
fun saveAuthToken(token: String) {
prefs.edit {
putString("auth_token", token) // Plain text!
}
}
fun saveUserPin(pin: String) {
prefs.edit {
putString("user_pin", pin) // Plain text!
}
}
fun getAuthToken(): String? {
return prefs.getString("auth_token", null)
}
}
```
**Issues:**
- 🔴 **Critical**: Auth token stored in plain text
- 🔴 **Critical**: User PIN stored in plain text
- 🔴 **Critical**: Security vulnerability - data can be read by rooted devices
### ✅ GOOD: EncryptedSharedPreferences
```kotlin
class SecureAuthManager(private val context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_auth",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveAuthToken(token: String) {
encryptedPrefs.edit {
putString(KEY_AUTH_TOKEN, token)
}
}
fun saveUserPin(pin: String) {
encryptedPrefs.edit {
putString(KEY_USER_PIN, pin)
}
}
fun getAuthToken(): String? {
return encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
}
fun clearAuthData() {
encryptedPrefs.edit {
remove(KEY_AUTH_TOKEN)
remove(KEY_USER_PIN)
}
}
companion object {
private const val KEY_AUTH_TOKEN = "auth_token"
private const val KEY_USER_PIN = "user_pin"
}
}
```
**Benefits:**
- ✅ Encrypted storage using AES256
- ✅ Protected against unauthorized access
- ✅ Secure even on rooted devices
- ✅ Clear API for data management
---
## Example 5: Inefficient RecyclerView
### ❌ BAD: notifyDataSetChanged and No ViewBinding
```kotlin
class PaymentAdapter : RecyclerView.Adapter<PaymentAdapter.ViewHolder>() {
private var payments: List<Payment> = emptyList()
fun updatePayments(newPayments: List<Payment>) {
payments = newPayments
notifyDataSetChanged() // Inefficient!
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_payment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(payments[position])
}
override fun getItemCount() = payments.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// Finding views repeatedly - inefficient!
fun bind(payment: Payment) {
itemView.findViewById<TextView>(R.id.amountTextView).text =
payment.amount.toString()
itemView.findViewById<TextView>(R.id.merchantTextView).text =
payment.merchantName
itemView.findViewById<TextView>(R.id.dateTextView).text =
payment.formattedDate
}
}
}
```
**Issues:**
- 🟡 **Medium**: notifyDataSetChanged() redraws entire list
- 🟡 **Medium**: findViewById called repeatedly in bind()
- 🟡 **Medium**: No ViewBinding - prone to errors
### ✅ GOOD: ListAdapter with DiffUtil and ViewBinding
```kotlin
class PaymentAdapter(
private val onItemClick: (Payment) -> Unit
) : ListAdapter<Payment, PaymentAdapter.ViewHolder>(PaymentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemPaymentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onItemClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(
private val binding: ItemPaymentBinding,
private val onItemClick: (Payment) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(payment: Payment) {
binding.apply {
amountTextView.text = payment.amount.formatAsCurrency()
merchantTextView.text = payment.merchantName
dateTextView.text = payment.formattedDate
statusBadge.text = payment.status
// Set status color
statusBadge.setBackgroundResource(
when (payment.status) {
"SUCCESS" -> R.drawable.bg_status_success
"PENDING" -> R.drawable.bg_status_pending
"FAILED" -> R.drawable.bg_status_failed
else -> R.drawable.bg_status_unknown
}
)
root.setOnClickListener {
onItemClick(payment)
}
}
}
}
}
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
override fun areItemsTheSame(oldItem: Payment, newItem: Payment): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Payment, newItem: Payment): Boolean {
return oldItem == newItem
}
}
// Usage in Fragment
class PaymentListFragment : Fragment() {
private val adapter = PaymentAdapter { payment ->
navigateToPaymentDetail(payment.id)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.payments.collect { payments ->
adapter.submitList(payments) // DiffUtil automatically calculates changes
}
}
}
}
```
**Benefits:**
- ✅ Efficient updates - only changed items redrawn
- ✅ ViewBinding - type-safe, no findViewById
- ✅ Automatic animation with DiffUtil
- ✅ Click handling with lambda
- ✅ Extension function for formatting
---
## Example 6: Fragment Lifecycle Issues
### ❌ BAD: Wrong Lifecycle Owner
```kotlin
class PaymentFragment : Fragment() {
private var _binding: FragmentPaymentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Using fragment's lifecycle - WRONG!
lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state) // Can crash after onDestroyView!
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
**Issues:**
- 🔴 **Critical**: Using fragment lifecycle instead of view lifecycle
- 🔴 **Critical**: Crash risk - accessing binding after onDestroyView
- 🟠 **High**: Memory leak between onDestroyView and onDestroy
### ✅ GOOD: Proper Lifecycle Management
```kotlin
class PaymentFragment : Fragment() {
private var _binding: FragmentPaymentBinding? = null
private val binding get() = _binding!!
private val viewModel: PaymentViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
observeViewModel()
}
private fun setupViews() {
binding.submitButton.setOnClickListener {
val amount = binding.amountEditText.text.toString()
viewModel.processPayment(amount)
}
}
private fun observeViewModel() {
// Use viewLifecycleOwner for UI updates
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
// Separate collection for one-time events
viewLifecycleOwner.lifecycleScope.launch {
viewModel.events.collect { event ->
handleEvent(event)
}
}
}
private fun updateUi(state: PaymentUiState) {
when (state) {
is PaymentUiState.Loading -> {
binding.progressBar.show()
binding.submitButton.isEnabled = false
}
is PaymentUiState.Success -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
showSuccessMessage(state.result)
}
is PaymentUiState.Error -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
showError(state.message)
}
is PaymentUiState.Initial -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
}
}
}
private fun handleEvent(event: PaymentEvent) {
when (event) {
is PaymentEvent.NavigateToReceipt -> {
findNavController().navigate(
PaymentFragmentDirections.actionToReceipt(event.transactionId)
)
}
is PaymentEvent.ShowError -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
**Benefits:**
- ✅ Correct lifecycle owner - viewLifecycleOwner
- ✅ No crash risk - collections cancelled when view destroyed
- ✅ Proper binding cleanup
- ✅ Separation of state and events
- ✅ Clean code organization
---
## Example 7: Hardcoded Configuration
### ❌ BAD: Hardcoded API Keys and URLs
```kotlin
class ApiClient {
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.payoo.vn/v1/") // Hardcoded!
.addConverterFactory(GsonConverterFactory.create())
.build()
private val apiKey = "sk_live_abc123xyz789" // Hardcoded secret!
fun getPaymentApi(): PaymentApi {
return retrofit.create(PaymentApi::class.java)
}
}
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Key", "sk_live_abc123xyz789") // Hardcoded!
.build()
return chain.proceed(request)
}
}
```
**Issues:**
- 🔴 **Critical**: API key exposed in source code
- 🟠 **High**: Can't switch between dev/staging/prod
- 🟠 **High**: Security risk if code is decompiled
### ✅ GOOD: BuildConfig and Gradle Properties
```kotlin
// gradle.properties (add to .gitignore)
API_KEY_DEBUG=sk_test_debug_key
API_KEY_RELEASE=sk_live_production_key
API_BASE_URL_DEBUG=https://api-dev.payoo.vn/v1/
API_BASE_URL_RELEASE=https://api.payoo.vn/v1/
// app/build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
}
buildTypes {
getByName("debug") {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
}
getByName("release") {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_RELEASE")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_RELEASE")}\"")
}
}
}
// NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideAuthInterceptor(): AuthInterceptor {
return AuthInterceptor(BuildConfig.API_KEY)
}
@Provides
@Singleton
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
class AuthInterceptor(private val apiKey: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Key", apiKey)
.addHeader("Content-Type", "application/json")
.build()
return chain.proceed(request)
}
}
```
**Benefits:**
- ✅ API keys not in source code
- ✅ Different configs for debug/release
- ✅ Secure - keys in gradle.properties (gitignored)
- ✅ Easy to manage different environments
- ✅ Dependency injection for testability
---
## Summary of Common Issues
### Critical (Fix Immediately)
1. Memory leaks (Activity/Context in ViewModel)
2. Coroutines not cancelled (GlobalScope)
3. Plain text storage for sensitive data
4. Hardcoded API keys/secrets
5. UI updates on background thread
### High Priority (Fix Soon)
6. Business logic in ViewModel
7. Direct API calls from ViewModel
8. Wrong lifecycle owner in Fragments
9. No error handling in coroutines
10. Wrong Dispatcher usage
### Medium Priority (Should Improve)
11. notifyDataSetChanged() instead of DiffUtil
12. findViewById instead of ViewBinding
13. No null safety checks
14. Poor naming conventions
15. Not using data classes
These examples provide concrete guidance for identifying and fixing common Android code issues during reviews.

View File

@@ -0,0 +1,953 @@
# Android Code Review Standards
Comprehensive standards for Android Kotlin development in the Payoo Android application.
## 1. Naming Conventions
### Types and Classes
```kotlin
// ✅ GOOD: PascalCase, descriptive
class PaymentViewModel
class TransactionRepository
interface UserDataSource
sealed class PaymentResult
// ❌ BAD: Abbreviations, unclear
class PmtVM
class TxnRepo
```
### Variables and Properties
```kotlin
// ✅ GOOD: camelCase, descriptive
val paymentAmount: BigDecimal
var isLoading: Boolean
private val transactionList: List<Transaction>
// ❌ BAD: Abbreviations, unclear
val amt: BigDecimal
var loading: Boolean
val txns: List<Transaction>
```
### Constants
```kotlin
// ✅ GOOD: UPPER_SNAKE_CASE
const val MAX_RETRY_COUNT = 3
const val API_BASE_URL = "https://api.payoo.vn"
private const val CACHE_DURATION_MS = 5000L
// ❌ BAD: Wrong case
const val maxRetryCount = 3
const val apiBaseUrl = "https://api.payoo.vn"
```
### Boolean Variables
```kotlin
// ✅ GOOD: Prefix with is, has, should, can
val isPaymentSuccessful: Boolean
val hasInternetConnection: Boolean
val shouldRetry: Boolean
val canProceed: Boolean
// ❌ BAD: No prefix
val paymentSuccessful: Boolean
val internetConnection: Boolean
```
### View IDs and Binding
```kotlin
// ✅ GOOD: Include type suffix
binding.amountEditText
binding.submitButton
binding.paymentRecyclerView
binding.errorTextView
// ❌ BAD: No type suffix
binding.amount
binding.submit
binding.payments
```
## 2. Kotlin Best Practices
### Null Safety
```kotlin
// ✅ GOOD: Safe calls and Elvis operator
val length = text?.length ?: 0
user?.name?.let { name ->
displayName(name)
}
// ❌ BAD: Force unwrap without checking
val length = text!!.length
displayName(user!!.name!!)
```
### Data Classes
```kotlin
// ✅ GOOD: Data classes for DTOs/Models
data class User(
val id: String,
val name: String,
val email: String
)
data class PaymentRequest(
val amount: BigDecimal,
val merchantId: String,
val currency: String = "VND"
)
// ❌ BAD: Regular class for simple data
class User {
var id: String = ""
var name: String = ""
var email: String = ""
}
```
### Sealed Classes for State
```kotlin
// ✅ GOOD: Sealed classes for state management
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
sealed class PaymentResult {
data class Success(val transactionId: String) : PaymentResult()
data class Failure(val error: PaymentError) : PaymentResult()
object Cancelled : PaymentResult()
}
// ❌ BAD: Using enums or nullable types
enum class State { LOADING, SUCCESS, ERROR }
```
### Immutability
```kotlin
// ✅ GOOD: Prefer val over var
val userId = getUserId()
val configuration = Config(apiKey, baseUrl)
// ❌ BAD: Unnecessary var
var userId = getUserId() // Never changed
var configuration = Config(apiKey, baseUrl) // Never changed
```
### Extension Functions
```kotlin
// ✅ GOOD: Extension functions for reusable utilities
fun String.isValidEmail(): Boolean {
return android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
}
fun BigDecimal.formatAsCurrency(): String {
return NumberFormat.getCurrencyInstance(Locale("vi", "VN")).format(this)
}
fun View.show() {
visibility = View.VISIBLE
}
fun View.hide() {
visibility = View.GONE
}
// Usage
if (email.isValidEmail()) {
binding.submitButton.show()
}
```
### Scope Functions
```kotlin
// ✅ GOOD: Appropriate scope function usage
// apply: Configure object
val user = User().apply {
name = "John"
email = "john@example.com"
}
// let: Null-safe operations
user?.let { u ->
saveUser(u)
}
// also: Side effects
val numbers = mutableListOf(1, 2, 3).also {
println("List created with ${it.size} elements")
}
// run: Execute block and return result
val result = repository.run {
fetchData()
processData()
}
// with: Multiple operations on object
with(binding) {
titleTextView.text = title
descriptionTextView.text = description
imageView.load(imageUrl)
}
```
## 3. Coroutines Patterns
### Proper Scope Usage
```kotlin
// ✅ GOOD: Use appropriate scope
class PaymentViewModel : ViewModel() {
fun loadPayments() {
viewModelScope.launch {
// Automatically cancelled when ViewModel cleared
val payments = repository.getPayments()
_uiState.value = UiState.Success(payments)
}
}
}
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// Cancelled when view is destroyed
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
// ❌ BAD: GlobalScope or runBlocking
GlobalScope.launch { // Never cancelled
repository.getPayments()
}
runBlocking { // Blocks thread
repository.getPayments()
}
```
### Dispatcher Usage
```kotlin
// ✅ GOOD: Appropriate dispatcher for task
class PaymentRepository(
private val api: PaymentApi,
private val database: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
val remotePayments = api.fetchPayments()
database.insertAll(remotePayments)
database.getAllPayments()
}
}
// ❌ BAD: Wrong dispatcher
class PaymentRepository {
suspend fun getPayments(): List<Payment> = withContext(Dispatchers.Main) {
// Network/DB work on Main thread!
api.fetchPayments()
}
}
```
### Error Handling
```kotlin
// ✅ GOOD: Proper error handling
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val result = repository.processPayment(request)
_uiState.value = UiState.Success(result)
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error: ${e.message}")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
}
}
// Or using runCatching
viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching {
repository.processPayment(request)
}.onSuccess { result ->
_uiState.value = UiState.Success(result)
}.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
// ❌ BAD: No error handling
viewModelScope.launch {
val result = repository.processPayment(request) // Can crash
_uiState.value = UiState.Success(result)
}
```
### Flow Usage
```kotlin
// ✅ GOOD: StateFlow for UI state
class PaymentViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<Payment>>(UiState.Loading)
val uiState: StateFlow<UiState<Payment>> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<PaymentEvent>()
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
fun processPayment(request: PaymentRequest) {
viewModelScope.launch {
repository.processPayment(request)
.catch { error ->
_uiState.value = UiState.Error(error.message)
}
.collect { result ->
_uiState.value = UiState.Success(result)
}
}
}
}
// ❌ BAD: LiveData with nullable types
class PaymentViewModel : ViewModel() {
val payment: MutableLiveData<Payment?> = MutableLiveData(null)
val error: MutableLiveData<String?> = MutableLiveData(null)
}
```
## 4. Clean Architecture
### Layer Structure
```
app/
├── data/
│ ├── datasource/ # API, Database, Cache
│ ├── repository/ # Implementation
│ └── model/ # DTOs, Entities
├── domain/
│ ├── model/ # Domain models
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic
└── presentation/
├── ui/ # Activities, Fragments
├── viewmodel/ # ViewModels
└── adapter/ # RecyclerView adapters
```
### ViewModel (Presentation Layer)
```kotlin
// ✅ GOOD: ViewModel uses UseCase, exposes UI state
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase,
private val getPaymentHistoryUseCase: GetPaymentHistoryUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
fun processPayment(amount: BigDecimal, merchantId: String) {
viewModelScope.launch {
_uiState.value = PaymentUiState.Loading
processPaymentUseCase(amount, merchantId)
.onSuccess { result ->
_uiState.value = PaymentUiState.Success(result)
}
.onFailure { error ->
_uiState.value = PaymentUiState.Error(error.message)
}
}
}
}
// ❌ BAD: ViewModel calls repository directly, has business logic
class PaymentViewModel(
private val repository: PaymentRepository
) : ViewModel() {
fun processPayment(amount: BigDecimal, merchantId: String) {
viewModelScope.launch {
// Business logic in ViewModel - WRONG!
if (amount < BigDecimal.ZERO) {
_error.value = "Invalid amount"
return@launch
}
val fee = amount * BigDecimal("0.02") // Business logic!
val total = amount + fee
// Calling repository directly - WRONG!
repository.processPayment(total, merchantId)
}
}
}
```
### UseCase (Domain Layer)
```kotlin
// ✅ GOOD: UseCase contains business logic
class ProcessPaymentUseCase(
private val paymentRepository: PaymentRepository,
private val feeCalculator: FeeCalculator,
private val validator: PaymentValidator
) {
suspend operator fun invoke(
amount: BigDecimal,
merchantId: String
): Result<PaymentResult> = runCatching {
// Business logic here
validator.validateAmount(amount)
validator.validateMerchant(merchantId)
val fee = feeCalculator.calculateFee(amount)
val totalAmount = amount + fee
val request = PaymentRequest(
amount = totalAmount,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
paymentRepository.processPayment(request)
}
}
// ❌ BAD: UseCase just forwards to repository
class ProcessPaymentUseCase(
private val repository: PaymentRepository
) {
suspend operator fun invoke(request: PaymentRequest) =
repository.processPayment(request) // No value added!
}
```
### Repository (Data Layer)
```kotlin
// ✅ GOOD: Repository abstracts data sources
interface PaymentRepository {
suspend fun processPayment(request: PaymentRequest): PaymentResult
suspend fun getPaymentHistory(): List<Payment>
}
class PaymentRepositoryImpl(
private val remoteDataSource: PaymentRemoteDataSource,
private val localDataSource: PaymentLocalDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun processPayment(
request: PaymentRequest
): PaymentResult = withContext(ioDispatcher) {
val result = remoteDataSource.processPayment(request)
localDataSource.savePayment(result.toEntity())
result
}
override suspend fun getPaymentHistory(): List<Payment> = withContext(ioDispatcher) {
try {
val remote = remoteDataSource.getPayments()
localDataSource.saveAll(remote.map { it.toEntity() })
remote
} catch (e: IOException) {
// Fallback to cache
localDataSource.getPayments().map { it.toDomain() }
}
}
}
// ❌ BAD: Repository has business logic
class PaymentRepositoryImpl(
private val api: PaymentApi
) : PaymentRepository {
override suspend fun processPayment(request: PaymentRequest): PaymentResult {
// Business logic in repository - WRONG!
if (request.amount < MIN_AMOUNT) {
throw IllegalArgumentException("Amount too low")
}
val fee = request.amount * FEE_RATE // Business logic!
val modifiedRequest = request.copy(amount = request.amount + fee)
return api.processPayment(modifiedRequest)
}
}
```
## 5. Lifecycle Management
### Activity/Fragment References
```kotlin
// ✅ GOOD: No Activity/Fragment references in ViewModel
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase
) : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
fun onPaymentSuccess() {
viewModelScope.launch {
_navigationEvent.emit(NavigationEvent.ToReceipt)
}
}
}
// In Fragment
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.navigationEvent.collect { event ->
when (event) {
NavigationEvent.ToReceipt -> navigateToReceipt()
}
}
}
}
}
// ❌ BAD: Holding Activity/Fragment reference
class PaymentViewModel(
private val fragment: PaymentFragment // Memory leak!
) : ViewModel() {
fun onPaymentSuccess() {
fragment.navigateToReceipt() // Crash if Fragment destroyed!
}
}
```
### Observer Lifecycle
```kotlin
// ✅ GOOD: Use viewLifecycleOwner in Fragments
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use viewLifecycleOwner, not this (fragment lifecycle)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
// ❌ BAD: Using fragment's lifecycle
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// This leaks between onDestroyView and onDestroy!
lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state) // Can crash if view is destroyed
}
}
}
}
```
### Cleanup
```kotlin
// ✅ GOOD: Proper cleanup
class PaymentViewModel : ViewModel() {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
// ❌ BAD: No cleanup
class PaymentViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.IO)
// No cleanup - scope keeps running!
}
```
## 6. Security Best Practices
### Secure Storage
```kotlin
// ✅ GOOD: Encrypted storage for sensitive data
class SecurePreferences(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String) {
encryptedPrefs.edit {
putString(KEY_AUTH_TOKEN, token)
}
}
}
// ❌ BAD: Plain SharedPreferences for sensitive data
class Preferences(context: Context) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
fun saveToken(token: String) {
prefs.edit {
putString("auth_token", token) // Plain text!
}
}
}
```
### API Keys
```kotlin
// ✅ GOOD: API keys in gradle.properties (not checked in) and BuildConfig
// gradle.properties (add to .gitignore)
API_KEY=your_secret_key_here
API_BASE_URL=https://api.payoo.vn
// build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL")}\"")
}
}
// Usage
class ApiClient {
private val apiKey = BuildConfig.API_KEY
private val baseUrl = BuildConfig.API_BASE_URL
}
// ❌ BAD: Hardcoded API keys
class ApiClient {
private val apiKey = "sk_live_1234567890abcdef" // Exposed in code!
private val baseUrl = "https://api.payoo.vn"
}
```
### Logging
```kotlin
// ✅ GOOD: No sensitive data in logs, conditional logging
class PaymentLogger {
fun logPayment(request: PaymentRequest) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Processing payment for merchant: ${request.merchantId}")
Log.d(TAG, "Amount: ${request.amount.setScale(2)}")
// NO card numbers, tokens, or PII
}
}
}
// ❌ BAD: Logging sensitive data
class PaymentLogger {
fun logPayment(request: PaymentRequest) {
Log.d(TAG, "Payment: $request") // Might contain sensitive data!
Log.d(TAG, "Token: ${request.authToken}") // Exposed in logs!
Log.d(TAG, "Card: ${request.cardNumber}") // PCI violation!
}
}
```
## 7. Performance Optimization
### Background Work
```kotlin
// ✅ GOOD: Network/DB on background thread
class PaymentRepository(
private val api: PaymentApi,
private val dao: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun syncPayments() = withContext(ioDispatcher) {
val remotePayments = api.fetchPayments()
dao.deleteAll()
dao.insertAll(remotePayments.map { it.toEntity() })
}
}
// ❌ BAD: Blocking Main thread
class PaymentRepository(
private val api: PaymentApi,
private val dao: PaymentDao
) {
fun syncPayments() {
// Blocking Main thread!
val remotePayments = api.fetchPayments().execute()
dao.deleteAll()
dao.insertAll(remotePayments.map { it.toEntity() })
}
}
```
### RecyclerView Optimization
```kotlin
// ✅ GOOD: DiffUtil for efficient updates
class PaymentAdapter : ListAdapter<Payment, PaymentViewHolder>(PaymentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentViewHolder {
val binding = ItemPaymentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PaymentViewHolder(binding)
}
override fun onBindViewHolder(holder: PaymentViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
override fun areItemsTheSame(oldItem: Payment, newItem: Payment) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Payment, newItem: Payment) =
oldItem == newItem
}
// ❌ BAD: notifyDataSetChanged for all updates
class PaymentAdapter : RecyclerView.Adapter<PaymentViewHolder>() {
private var payments: List<Payment> = emptyList()
fun updatePayments(newPayments: List<Payment>) {
payments = newPayments
notifyDataSetChanged() // Inefficient!
}
}
```
### Image Loading
```kotlin
// ✅ GOOD: Coil/Glide with proper caching
class ImageLoader(private val context: Context) {
fun loadImage(imageView: ImageView, url: String) {
imageView.load(url) {
crossfade(true)
placeholder(R.drawable.placeholder)
error(R.drawable.error)
transformations(CircleCropTransformation())
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
}
}
}
// ❌ BAD: Loading images without caching
class ImageLoader {
suspend fun loadImage(imageView: ImageView, url: String) {
val bitmap = withContext(Dispatchers.IO) {
URL(url).openStream().use { stream ->
BitmapFactory.decodeStream(stream) // No caching, inefficient!
}
}
imageView.setImageBitmap(bitmap)
}
}
```
### Database Optimization
```kotlin
// ✅ GOOD: Indexed queries, efficient types
@Entity(
tableName = "payments",
indices = [
Index(value = ["merchantId"]),
Index(value = ["timestamp"])
]
)
data class PaymentEntity(
@PrimaryKey val id: String,
val merchantId: String,
val amount: Long, // Store cents, not BigDecimal
val timestamp: Long,
val status: String
)
@Dao
interface PaymentDao {
@Query("SELECT * FROM payments WHERE merchantId = :merchantId AND timestamp > :after ORDER BY timestamp DESC")
fun getPaymentsByMerchant(merchantId: String, after: Long): Flow<List<PaymentEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(payments: List<PaymentEntity>)
}
// ❌ BAD: No indexes, inefficient queries
@Entity(tableName = "payments")
data class PaymentEntity(
@PrimaryKey val id: String,
val merchantId: String,
val amount: BigDecimal, // Not supported by Room!
val timestamp: Long,
val status: String
)
@Dao
interface PaymentDao {
@Query("SELECT * FROM payments")
fun getAllPayments(): List<PaymentEntity> // Load all, then filter!
}
```
## 8. Dependency Injection
### Hilt/Dagger Setup
```kotlin
// ✅ GOOD: Proper DI with Hilt
@HiltAndroidApp
class PayooApplication : Application()
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun providePaymentApi(retrofit: Retrofit): PaymentApi {
return retrofit.create(PaymentApi::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindPaymentRepository(
impl: PaymentRepositoryImpl
): PaymentRepository
}
@AndroidEntryPoint
class PaymentActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: PaymentViewModelFactory
}
// ❌ BAD: Manual instantiation, singletons
object ApiClient {
val instance: PaymentApi by lazy {
Retrofit.Builder()
.baseUrl("https://api.payoo.vn")
.build()
.create(PaymentApi::class.java)
}
}
class PaymentViewModel {
private val repository = PaymentRepository(ApiClient.instance) // Tight coupling!
}
```
---
## Summary Checklist
Use this checklist during code reviews:
### Naming ✅
- [ ] Classes/Interfaces in PascalCase
- [ ] Variables/Functions in camelCase
- [ ] Constants in UPPER_SNAKE_CASE
- [ ] Boolean variables prefixed (is, has, should, can)
- [ ] No unclear abbreviations
### Kotlin ✅
- [ ] Null safety properly handled
- [ ] Data classes for DTOs/models
- [ ] Sealed classes for states
- [ ] Prefer val over var
- [ ] Extension functions where appropriate
### Coroutines ✅
- [ ] Proper scope usage (viewModelScope, lifecycleScope)
- [ ] Correct dispatchers (IO for network/DB, Main for UI)
- [ ] Error handling in all coroutines
- [ ] Flows for reactive data
- [ ] No runBlocking in production
### Architecture ✅
- [ ] Clear layer separation (Presentation/Domain/Data)
- [ ] ViewModels use UseCases
- [ ] UseCases contain business logic
- [ ] Repositories abstract data sources
- [ ] Dependency injection configured
### Lifecycle ✅
- [ ] No Activity/Fragment references in ViewModel
- [ ] viewLifecycleOwner in Fragments
- [ ] Proper cleanup in onCleared
- [ ] Configuration changes handled
### Security ✅
- [ ] EncryptedSharedPreferences for sensitive data
- [ ] No hardcoded API keys/secrets
- [ ] No sensitive data in logs
- [ ] HTTPS only
- [ ] Input validation
### Performance ✅
- [ ] Network/DB on background threads
- [ ] No memory leaks
- [ ] DiffUtil in RecyclerView
- [ ] Image caching (Coil/Glide)
- [ ] Database queries optimized
This comprehensive standards document provides detailed guidance for Android code reviews in the Payoo Android application.

View File

@@ -0,0 +1,181 @@
---
name: clean-architecture-review
description: Validate Clean Architecture implementation in iOS. Checks layer separation (Presentation/Domain/Data), MVVM patterns, dependency injection with Swinject, and UseCase/Repository patterns. Use when reviewing architecture, checking layer boundaries, or validating DI.
allowed-tools: Read, Grep, Glob
---
# Clean Architecture Validator
Verify Clean Architecture and MVVM implementation in iOS code following Payoo Merchant patterns.
## When to Activate
- "architecture review", "layer separation", "clean architecture"
- "MVVM", "dependency injection", "DI"
- "use case", "repository pattern"
- Reviewing module structure or refactoring
## Architecture Layers
**Presentation** → ViewControllers, ViewModels, Views
**Domain** → UseCases (business logic), Models, Repository protocols
**Data** → Repository implementations, API Services, Local Storage
**Correct Flow**:
```
UI → ViewController → ViewModel → UseCase → Repository → API/DB
```
## Review Process
### Step 1: Map Architecture
Classify files into layers:
- Presentation: `*ViewController.swift`, `*ViewModel.swift`
- Domain: `*UseCase.swift`, `*Repository.swift` (protocols)
- Data: `*RepositoryImpl.swift`, `*ApiService.swift`
### Step 2: Check Layer Violations
**Critical Issues**:
- 🔴 ViewModel calling API directly (bypassing UseCase)
- 🔴 Business logic in ViewModel (should be in UseCase)
- 🔴 UseCase calling API directly (bypassing Repository)
- 🔴 Direct instantiation (no DI)
### Step 3: Verify Patterns
**BaseViewModel**:
```swift
class PaymentViewModel: BaseViewModel<PaymentState>
class PaymentViewModel // Should extend BaseViewModel
```
**UseCase Pattern**:
```swift
protocol PaymentUseCase { }
class PaymentUseCaseImpl: PaymentUseCase { }
class PaymentUseCase { } // Should be protocol + impl
```
**Repository Pattern**:
```swift
protocol PaymentRepository { } // In Domain
class PaymentRepositoryImpl: PaymentRepository { } // In Data
```
**Dependency Injection**:
```swift
init(paymentUC: PaymentUseCase) { // Constructor injection
self.paymentUC = paymentUC
}
let paymentUC = PaymentUseCaseImpl() // Direct instantiation
```
### Step 4: Generate Report
Provide:
- Architecture compliance score
- Layer violations by severity
- Current vs. should-be architecture
- Refactoring steps
- Effort estimate
## Common Violations
### ❌ ViewModel Bypassing UseCase
```swift
class PaymentViewModel {
private let apiService: PaymentApiService // WRONG LAYER!
}
```
**Should be**:
```swift
class PaymentViewModel {
private let paymentUC: PaymentUseCase // CORRECT!
}
```
### ❌ Business Logic in ViewModel
```swift
class PaymentViewModel {
func processPayment(amount: Double) {
// Validation in ViewModel
guard amount > 1000 else { return }
// Business rules in ViewModel
let fee = amount * 0.01
}
}
```
**Should be in UseCase**:
```swift
class PaymentUseCaseImpl {
func execute(amount: Double) -> Single<PaymentResult> {
// Validation in UseCase
return validateAmount(amount)
.flatMap { processPayment($0) }
}
}
```
## Output Format
```markdown
# Clean Architecture Review
## Compliance Score: X/100
## Critical Violations: X
### 1. ViewModel Bypassing UseCase
**File**: `PaymentViewModel.swift:15`
**Current**: ViewModel → API
**Should be**: ViewModel → UseCase → Repository → API
**Fix**: [Refactoring steps]
---
## Dependency Graph
### Current (Problematic)
ViewModel → ApiService ❌
### Should Be
ViewModel → UseCase → Repository → ApiService ✅
## Recommendations
1. Create missing UseCases
2. Move business logic to Domain layer
3. Setup DI container
4. Add Repository layer
## Effort Estimate
- Module refactoring: X hours
- DI setup: X hours
- Testing: X hours
```
## Quick Checks
**Layer Boundaries**:
- [ ] ViewModels only depend on UseCases
- [ ] UseCases contain all business logic
- [ ] Repositories handle data access only
- [ ] No UI code in Domain/Data layers
**Dependency Injection**:
- [ ] All dependencies via constructor
- [ ] No direct instantiation
- [ ] Swinject container registration
- [ ] Protocol-based dependencies
**Patterns**:
- [ ] ViewModels extend BaseViewModel
- [ ] UseCases follow protocol + impl
- [ ] Repositories follow protocol + impl
- [ ] State management via setState()
## Reference
**Detailed Examples**: See `examples.md` for complete architecture patterns and refactoring guides.

View File

@@ -0,0 +1,505 @@
# Clean Architecture Examples
Complete examples of proper layer separation, MVVM patterns, and dependency injection.
## Complete Architecture Example
### Proper 3-Layer Implementation
#### Presentation Layer - ViewModel
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
// Depends only on UseCase (Domain layer)
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
// UI State
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessing = BehaviorRelay<Bool>(value: false)
// Constructor injection
init(paymentUC: PaymentUseCase) {
self.paymentUC = paymentUC
super.init()
}
func processPayment() {
isProcessing.accept(true)
// Delegates to UseCase, no business logic here
paymentUC.execute(amount: paymentAmount.value)
.subscribeOn(ConcurrentScheduler.background)
.observeOn(MainScheduler.instance)
.subscribe(
onNext: { [weak self] result in
self?.setState(.success(result))
},
onError: { [weak self] error in
self?.setState(.error(error))
},
onCompleted: { [weak self] in
self?.isProcessing.accept(false)
}
)
.disposed(by: disposeBag)
}
}
```
#### Domain Layer - UseCase
```swift
// Protocol in Domain layer
protocol PaymentUseCase {
func execute(amount: String) -> Single<PaymentResult>
}
// Implementation in Domain layer
class PaymentUseCaseImpl: PaymentUseCase {
// Depends on Repository protocol (Domain layer)
private let paymentRepository: PaymentRepository
private let validationService: ValidationService
init(paymentRepository: PaymentRepository,
validationService: ValidationService) {
self.paymentRepository = paymentRepository
self.validationService = validationService
}
func execute(amount: String) -> Single<PaymentResult> {
// Business logic in UseCase
return validationService.validateAmount(amount)
.flatMap { validatedAmount in
// Calls Repository, not API directly
return self.paymentRepository.processPayment(amount: validatedAmount)
}
.map { response in
// Business rules applied here
return self.applyBusinessRules(response)
}
}
private func applyBusinessRules(_ response: PaymentResponse) -> PaymentResult {
// Business logic here
return PaymentResult(from: response)
}
}
```
#### Data Layer - Repository
```swift
// Protocol in Domain layer
protocol PaymentRepository {
func processPayment(amount: Double) -> Single<PaymentResponse>
}
// Implementation in Data layer
class PaymentRepositoryImpl: PaymentRepository {
private let apiService: PaymentApiService
private let localStorage: PaymentLocalStorage
init(apiService: PaymentApiService, localStorage: PaymentLocalStorage) {
self.apiService = apiService
self.localStorage = localStorage
}
func processPayment(amount: Double) -> Single<PaymentResponse> {
// Data access only, no business logic
return apiService.processPayment(amount: amount)
.do(onSuccess: { [weak self] response in
// Save to local storage
self?.localStorage.savePaymentRecord(response)
})
}
}
```
#### DI Setup - Swinject
```swift
extension Container {
func registerPaymentModule() {
// Register Use Cases
register(PaymentUseCase.self) { resolver in
PaymentUseCaseImpl(
paymentRepository: resolver.resolve(PaymentRepository.self)!,
validationService: resolver.resolve(ValidationService.self)!
)
}
// Register Repositories
register(PaymentRepository.self) { resolver in
PaymentRepositoryImpl(
apiService: resolver.resolve(PaymentApiService.self)!,
localStorage: resolver.resolve(PaymentLocalStorage.self)!
)
}
// Register ViewModels
register(PaymentViewModel.self) { resolver in
PaymentViewModel(
paymentUC: resolver.resolve(PaymentUseCase.self)!
)
}
}
}
```
---
## Common Anti-Patterns
### ❌ Anti-Pattern 1: ViewModel Calling API Directly
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
private let apiService: PaymentApiService // Wrong layer!
func processPayment(amount: Double) {
// Direct API call bypasses business logic
apiService.processPayment(amount: amount)
.subscribe(onNext: { result in
// Handle result
})
.disposed(by: disposeBag)
}
}
```
**Problems**:
- Business logic scattered or missing
- Hard to test
- Violates layer separation
- Cannot reuse logic elsewhere
**Fix**: Add UseCase layer
```swift
// 1. Create UseCase
protocol PaymentUseCase {
func execute(amount: Double) -> Single<PaymentResult>
}
// 2. Update ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase // Correct!
func processPayment(amount: Double) {
paymentUC.execute(amount: amount) // Through UseCase
.subscribe(/* ... */)
.disposed(by: disposeBag)
}
}
```
---
### ❌ Anti-Pattern 2: Business Logic in ViewModel
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
func processPayment(amount: String) {
// Validation logic in ViewModel
guard let amt = Double(amount), amt > 1000 else {
setState(.showError("Invalid amount"))
return
}
// Business rules in ViewModel
let fee = amt * 0.01
let total = amt + fee
if total > 50_000_000 {
setState(.showError("Amount too high"))
return
}
// Process...
}
}
```
**Problems**:
- Cannot reuse validation/business logic
- Hard to test logic independently
- ViewModel becomes complex
- Violates Single Responsibility
**Fix**: Move to UseCase
```swift
// Business logic in UseCase
class PaymentUseCaseImpl: PaymentUseCase {
func execute(amount: String) -> Single<PaymentResult> {
return validateAmount(amount)
.flatMap { validatedAmount in
return self.calculateFeesAndProcess(validatedAmount)
}
}
private func validateAmount(_ amount: String) -> Single<Double> {
guard let amt = Double(amount), amt > 1000 else {
return .error(PaymentError.invalidAmount)
}
let fee = amt * 0.01
let total = amt + fee
guard total <= 50_000_000 else {
return .error(PaymentError.amountTooHigh)
}
return .just(amt)
}
}
// ViewModel simplified
class PaymentViewModel: BaseViewModel<PaymentState> {
func processPayment(amount: String) {
paymentUC.execute(amount: amount)
.subscribe(
onNext: { [weak self] result in
self?.setState(.success(result))
},
onError: { [weak self] error in
self?.setState(.error(error))
}
)
.disposed(by: disposeBag)
}
}
```
---
### ❌ Anti-Pattern 3: UseCase Calling API Directly
```swift
class PaymentUseCaseImpl: PaymentUseCase {
private let apiService: PaymentApiService // Bypasses Repository!
func execute(amount: Double) -> Single<PaymentResult> {
// Direct API call
return apiService.processPayment(amount: amount)
.map { response in
return PaymentResult(from: response)
}
}
}
```
**Problems**:
- Cannot swap data sources (API/local/mock)
- Hard to test independently
- Violates Dependency Inversion
**Fix**: Add Repository layer
```swift
// Repository protocol in Domain
protocol PaymentRepository {
func processPayment(amount: Double) -> Single<PaymentResponse>
}
// UseCase depends on protocol
class PaymentUseCaseImpl: PaymentUseCase {
private let paymentRepository: PaymentRepository // Correct!
func execute(amount: Double) -> Single<PaymentResult> {
return paymentRepository.processPayment(amount: amount)
.map { response in
return PaymentResult(from: response)
}
}
}
// Implementation in Data layer
class PaymentRepositoryImpl: PaymentRepository {
private let apiService: PaymentApiService
func processPayment(amount: Double) -> Single<PaymentResponse> {
return apiService.processPayment(amount: amount)
}
}
```
---
## Refactoring Examples
### Example: Refactoring ViewModel with Business Logic
#### Before (Violates Clean Architecture)
```swift
class TransactionViewModel: BaseViewModel<TransactionState> {
private let apiService: TransactionApiService
func loadTransactions(from startDate: Date, to endDate: Date) {
// Date validation in ViewModel
guard startDate <= endDate else {
setState(.showError("Invalid date range"))
return
}
// Business rule in ViewModel
let daysDiff = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day!
guard daysDiff <= 90 else {
setState(.showError("Date range too large"))
return
}
// Direct API call
apiService.getTransactions(from: startDate, to: endDate)
.subscribe(onNext: { [weak self] transactions in
// Business logic: filtering
let filtered = transactions.filter { $0.amount > 0 }
self?.transactions.accept(filtered)
})
.disposed(by: disposeBag)
}
}
```
#### After (Clean Architecture)
```swift
// DOMAIN LAYER - UseCase
protocol TransactionUseCase {
func getTransactions(from: Date, to: Date) -> Single<[Transaction]>
}
class TransactionUseCaseImpl: TransactionUseCase {
private let repository: TransactionRepository
func getTransactions(from startDate: Date, to endDate: Date) -> Single<[Transaction]> {
// Validation in UseCase
return validateDateRange(startDate, endDate)
.flatMap { _ in
return self.repository.getTransactions(from: startDate, to: endDate)
}
.map { transactions in
// Business logic in UseCase
return self.filterValidTransactions(transactions)
}
}
private func validateDateRange(_ start: Date, _ end: Date) -> Single<Void> {
guard start <= end else {
return .error(TransactionError.invalidDateRange)
}
let daysDiff = Calendar.current.dateComponents([.day], from: start, to: end).day!
guard daysDiff <= 90 else {
return .error(TransactionError.dateRangeTooLarge)
}
return .just(())
}
private func filterValidTransactions(_ transactions: [Transaction]) -> [Transaction] {
return transactions.filter { $0.amount > 0 }
}
}
// PRESENTATION LAYER - ViewModel
class TransactionViewModel: BaseViewModel<TransactionState> {
private let transactionUC: TransactionUseCase // Depends on UseCase
func loadTransactions(from startDate: Date, to endDate: Date) {
// Simply delegates to UseCase
transactionUC.getTransactions(from: startDate, to: endDate)
.subscribe(
onNext: { [weak self] transactions in
self?.transactions.accept(transactions)
self?.setState(.loaded)
},
onError: { [weak self] error in
self?.setState(.error(error))
}
)
.disposed(by: disposeBag)
}
}
```
**Benefits**:
- ✅ Clear separation of concerns
- ✅ Business logic testable in isolation
- ✅ ViewModel is simple coordinator
- ✅ Can reuse UseCase elsewhere
---
## Testing Benefits
### With Clean Architecture
```swift
class PaymentUseCaseTests: XCTestCase {
func testExecute_WithValidAmount_ProcessesPayment() {
// Can test UseCase in isolation
let mockRepository = MockPaymentRepository()
mockRepository.processPaymentResult = .just(PaymentResponse.success)
let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository)
// Test business logic directly
let result = try! useCase.execute(amount: "10000").toBlocking().first()!
XCTAssertTrue(result.isSuccess)
}
func testExecute_WithInvalidAmount_ReturnsError() {
let mockRepository = MockPaymentRepository()
let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository)
// Test validation logic
XCTAssertThrowsError(
try useCase.execute(amount: "500").toBlocking().first()
) { error in
XCTAssertEqual(error as? PaymentError, .invalidAmount)
}
}
}
```
### Without Clean Architecture
```swift
// Hard to test business logic without UI/Network
class PaymentViewModelTests: XCTestCase {
func testProcessPayment() {
// Need to mock UI, network, and test everything together
// Business logic mixed with presentation logic
// Hard to isolate what's being tested
}
}
```
---
## Checklist for Reviews
```markdown
## Clean Architecture Checklist
### Layer Separation
- [ ] ViewModel only depends on UseCase (not API/Repository)
- [ ] UseCase contains all business logic
- [ ] UseCase only depends on Repository protocol
- [ ] Repository implementation is in Data layer
- [ ] No business logic in ViewModel
- [ ] No business logic in Repository
- [ ] No UI code in Domain/Data layers
### Patterns
- [ ] ViewModel extends BaseViewModel<State>
- [ ] UseCase follows protocol + implementation pattern
- [ ] Repository follows protocol + implementation pattern
- [ ] State management uses setState()
### Dependency Injection
- [ ] All dependencies injected via constructor
- [ ] No direct instantiation of dependencies
- [ ] Swinject container properly configured
- [ ] Dependencies are protocol-based (not concrete types)
### Data Flow
- [ ] UI → ViewController → ViewModel → UseCase → Repository → API/DB
- [ ] Never skips layers
- [ ] Observables/Singles for async operations
- [ ] Errors properly propagated through layers
```

View File

@@ -0,0 +1,163 @@
---
name: copilot-agent-builder
description: Generate custom GitHub Copilot agents (.agent.md files) for VS Code with proper YAML frontmatter, tools configuration, and handoff workflows. Use when "create copilot agent", "generate github copilot agent", "new copilot agent for", "make a copilot agent", or "build copilot agent".
allowed-tools:
- Read
- Write
- Glob
- Bash
- AskUserQuestion
---
# GitHub Copilot Agent Builder
Generate custom GitHub Copilot agents following VS Code's `.agent.md` format with proper YAML frontmatter and markdown instructions.
## When to Activate
Trigger this skill when user says:
- "create copilot agent"
- "generate github copilot agent"
- "new copilot agent for [purpose]"
- "make a copilot agent"
- "build copilot agent"
- "copilot custom agent"
## Agent Creation Process
### Step 1: Gather Requirements
Ask user for agent configuration:
1. **Agent Purpose & Name**:
- What should the agent do? (e.g., "plan features", "review security", "write tests")
- Suggested name: Extract from purpose (e.g., "planner", "security-reviewer", "test-writer")
2. **Tools Selection** (multi-select):
- `fetch` - Retrieve web content and documentation
- `search` - Search codebase and files
- `githubRepo` - Access GitHub repository data
- `usages` - Find code references and usage patterns
- `files` - File operations
- Custom tools if available
3. **Handoff Workflows** (optional):
- Should this agent hand off to another? (e.g., planner → coder)
- Handoff agent name
- Handoff prompt text
- Auto-send handoff? (yes/no)
4. **Additional Configuration** (optional):
- Specific model to use?
- Argument hint for users?
### Step 2: Validate Directory Structure
```bash
# Ensure .github/agents directory exists
mkdir -p .github/agents
```
### Step 3: Generate Agent File
Create `.github/agents/{agent-name}.agent.md` with:
**YAML Frontmatter Structure**:
```yaml
---
description: [Brief overview shown in chat input]
name: [Agent identifier, lowercase with hyphens]
tools: [Array of tool names]
handoffs: # Optional
- label: [Button text]
agent: [Target agent name]
prompt: [Handoff message]
send: [true/false - auto-submit?]
model: [Optional - specific model name]
argument-hint: [Optional - user guidance text]
---
```
**Markdown Body**:
- Clear role definition
- Specific instructions
- Tool usage guidance (reference as `#tool:toolname`)
- Output format expectations
- Constraints and guidelines
### Step 4: Validate Format
Ensure generated file has:
- ✓ Valid YAML frontmatter with `---` delimiters
- ✓ All required fields (description, name)
- ✓ Tools array properly formatted
- ✓ Handoffs array if specified (with label, agent, prompt, send)
- ✓ Markdown instructions that are clear and actionable
### Step 5: Confirm Creation
Show user:
```markdown
✅ GitHub Copilot Agent Created
📁 Location: `.github/agents/{name}.agent.md`
🎯 Agent: {name}
📝 Description: {description}
🛠️ Tools: {tools list}
To use:
1. Reload VS Code window (Cmd/Ctrl + Shift + P → "Reload Window")
2. Open GitHub Copilot Chat
3. Type `@{name}` to invoke your custom agent
{If handoffs configured: "This agent can hand off to: {handoff targets}"}
```
## Output Format
After creation, provide:
1. **File path confirmation**
2. **Configuration summary** (name, description, tools, handoffs)
3. **Usage instructions** (how to reload and use)
4. **Next steps** (testing suggestions)
## Reference Documentation
- Templates and structures → `templates.md`
- Real-world examples → `examples.md`
## Important Guidelines
1. **Naming Convention**: Use lowercase with hyphens (e.g., `feature-planner`, `security-reviewer`)
2. **Description**: Brief (1-2 sentences), shown in chat input UI
3. **Tools**: Only include tools the agent actually needs
4. **Handoffs**: Enable multi-step workflows (planning → implementation → testing)
5. **Instructions**: Be specific about what the agent should and shouldn't do
6. **Tool References**: Use `#tool:toolname` syntax in markdown body
## Common Agent Patterns
**Planning Agent**:
- Tools: `fetch`, `search`, `githubRepo`, `usages`
- Handoff: To implementation agent
- Focus: Analysis and planning, no code edits
**Implementation Agent**:
- Tools: `search`, `files`, `usages`
- Handoff: To testing agent
- Focus: Write and modify code
**Review Agent**:
- Tools: `search`, `githubRepo`, `usages`
- Handoff: Back to implementation for fixes
- Focus: Quality, security, performance checks
**Testing Agent**:
- Tools: `search`, `files`
- Handoff: Back to implementation or to review
- Focus: Test creation and validation
---
**Ready to build custom GitHub Copilot agents!**

View File

@@ -0,0 +1,651 @@
# GitHub Copilot Agent Examples
## Example 1: Feature Planner Agent
**Use Case**: Planning new features without implementation
```markdown
---
description: Analyze requirements and create detailed implementation plans
name: feature-planner
tools: ['fetch', 'search', 'githubRepo', 'usages']
handoffs:
- label: Start Implementation
agent: agent
prompt: Implement the feature plan outlined above step by step.
send: false
model: gpt-4
argument-hint: Describe the feature you want to plan
---
# Feature Planning Mode
You are in planning mode. Your goal is to analyze requirements and create comprehensive implementation plans WITHOUT making any code changes.
## Process
1. **Understand Requirements**: Ask clarifying questions about the feature
2. **Research Context**: Use #tool:search to understand existing codebase patterns
3. **Design Solution**: Create architecture and implementation approach
4. **Plan Steps**: Break down into actionable implementation steps
5. **Identify Risks**: Note potential challenges and dependencies
## Research Tools
- Use #tool:fetch to retrieve documentation for libraries or best practices
- Use #tool:githubRepo to analyze repository structure and conventions
- Use #tool:usages to find how similar features are implemented
- Use #tool:search to locate relevant existing code
## Output Structure
Create a plan with these sections:
**Requirements Analysis**
- Feature overview
- User stories
- Acceptance criteria
**Technical Design**
- Architecture decisions
- Component interactions
- Data flow
**Implementation Plan**
1. File structure changes needed
2. Step-by-step implementation sequence
3. Configuration updates
4. Database migrations (if applicable)
**Testing Strategy**
- Unit test requirements
- Integration test scenarios
- Manual testing checklist
**Risks & Considerations**
- Technical challenges
- Dependencies on other systems
- Performance implications
- Security considerations
## Important Guidelines
- **NO CODE CHANGES**: You only create plans, never implement
- **BE SPECIFIC**: Include file paths, function names, specific changes
- **CONSIDER CONTEXT**: Align with existing patterns and conventions
- **IDENTIFY GAPS**: Note missing information or unclear requirements
- **ENABLE HANDOFF**: Make plans detailed enough for another agent to implement
After completing the plan, offer the "Start Implementation" handoff to transition to the coding agent.
```
## Example 2: Security Reviewer Agent
**Use Case**: Security-focused code review
```markdown
---
description: Review code for security vulnerabilities and best practices
name: security-reviewer
tools: ['search', 'githubRepo', 'usages']
handoffs:
- label: Fix Security Issues
agent: agent
prompt: Address the security vulnerabilities identified in the review.
send: false
---
# Security Code Review Agent
You are a security-focused code reviewer specializing in identifying vulnerabilities and enforcing security best practices.
## Security Review Checklist
### Input Validation
- [ ] All user inputs are validated and sanitized
- [ ] No SQL injection vulnerabilities
- [ ] No command injection vulnerabilities
- [ ] File paths are validated against directory traversal
### Authentication & Authorization
- [ ] Authentication is properly implemented
- [ ] Authorization checks are in place
- [ ] Session management is secure
- [ ] Passwords are hashed (never plain text)
- [ ] Multi-factor authentication considered
### Data Protection
- [ ] Sensitive data is encrypted at rest
- [ ] TLS/HTTPS used for data in transit
- [ ] No secrets in code or version control
- [ ] PII is properly protected
### Common Vulnerabilities (OWASP Top 10)
- [ ] No XSS vulnerabilities
- [ ] No CSRF vulnerabilities
- [ ] No insecure deserialization
- [ ] No XML external entity (XXE) attacks
- [ ] Proper security headers configured
### Dependencies
- [ ] Dependencies are up to date
- [ ] No known vulnerable dependencies
- [ ] License compliance checked
## Review Process
1. Use #tool:search to scan for common vulnerability patterns
2. Use #tool:githubRepo to check security configuration files
3. Use #tool:usages to verify security functions are used correctly
4. Cross-reference against OWASP Top 10 and CWE database
## Output Format
```markdown
## Security Review Report
🔒 **Security Status**: [PASS / NEEDS ATTENTION / CRITICAL]
### 🔴 Critical Vulnerabilities (Fix Immediately)
- [ ] **File**: `path/to/file.ts:42`
- **Issue**: SQL Injection vulnerability
- **Details**: User input directly concatenated into SQL query
- **Fix**: Use parameterized queries
- **Example**:
```typescript
// ❌ Vulnerable
const query = `SELECT * FROM users WHERE id = ${userId}`;
// ✅ Secure
const query = 'SELECT * FROM users WHERE id = ?';
db.execute(query, [userId]);
```
### 🟡 Medium Priority Issues
- [ ] **File**: `path/to/file.ts:67`
- **Issue**: [Description]
- **Fix**: [Recommendation]
### ✅ Security Strengths
- Proper input validation in authentication flow
- TLS configured correctly
- Security headers implemented
### 📊 Summary
- **Critical**: 1
- **High**: 0
- **Medium**: 2
- **Low**: 3
### Recommended Actions
1. [Priority 1 action]
2. [Priority 2 action]
```
Use "Fix Security Issues" handoff to address vulnerabilities.
```
## Example 3: API Documentation Generator
**Use Case**: Automatic API documentation
```markdown
---
description: Generate comprehensive API documentation from code
name: api-documenter
tools: ['search', 'files', 'githubRepo']
---
# API Documentation Generator
You are a technical writer specializing in API documentation. Generate clear, comprehensive documentation for APIs.
## Documentation Process
1. **Discover APIs**: Use #tool:search to find API endpoints and functions
2. **Analyze Structure**: Use #tool:githubRepo to understand project organization
3. **Extract Details**: Parse parameters, return types, and examples
4. **Generate Docs**: Create structured documentation files
## Documentation Format
For each API endpoint or function, document:
### REST API Endpoint
```markdown
### [HTTP METHOD] `/api/endpoint/path`
**Description**: [What this endpoint does]
**Authentication**: [Required/Optional - Type]
**Parameters**:
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `param1` | string | Yes | [Description] |
| `param2` | number | No | [Description] |
**Request Body**:
```json
{
"field1": "value",
"field2": 123
}
```
**Response**:
```json
{
"status": "success",
"data": {
"id": "123",
"name": "Example"
}
}
```
**Error Responses**:
- `400 Bad Request`: Invalid parameters
- `401 Unauthorized`: Missing or invalid authentication
- `404 Not Found`: Resource not found
- `500 Internal Server Error`: Server error
**Example**:
```bash
curl -X POST https://api.example.com/api/endpoint/path \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"field1": "value", "field2": 123}'
```
```
### Function/Method
```markdown
### `functionName(param1, param2)`
**Description**: [What this function does]
**Parameters**:
- `param1` (Type): [Description]
- `param2` (Type): [Description]
**Returns**: [Return type and description]
**Throws**: [Exceptions if any]
**Example**:
```typescript
const result = functionName('value', 42);
console.log(result); // Expected output
```
```
## Output Structure
Create documentation files following this hierarchy:
```
docs/
├── api/
│ ├── README.md (Overview)
│ ├── authentication.md
│ ├── endpoints/
│ │ ├── users.md
│ │ ├── products.md
│ │ └── orders.md
│ └── errors.md
└── guides/
├── getting-started.md
└── examples.md
```
## Guidelines
- Include working code examples for all endpoints
- Document error cases and status codes
- Provide authentication examples
- Show request/response examples with real data structures
- Include rate limiting information
- Link related endpoints
- Keep examples up to date with code changes
```
## Example 4: Test Generator Agent
**Use Case**: Automatic test creation
```markdown
---
description: Generate comprehensive test suites with high coverage
name: test-generator
tools: ['search', 'files', 'usages']
handoffs:
- label: Run Tests
agent: agent
prompt: Execute the generated tests and report results.
send: true
---
# Comprehensive Test Generator
You are a testing specialist who creates thorough, maintainable test suites.
## Test Generation Process
1. **Analyze Code**: Use #tool:search to understand what needs testing
2. **Find Patterns**: Use #tool:usages to see how code is used in practice
3. **Create Tests**: Use #tool:files to generate test files
4. **Ensure Coverage**: Cover happy paths, edge cases, and error scenarios
## Test Structure
### Unit Test Template
```typescript
import { describe, it, expect, beforeEach, afterEach } from '@testing-framework';
import { FunctionToTest } from '../src/module';
describe('FunctionToTest', () => {
let testContext: TestContext;
beforeEach(() => {
// Setup
testContext = createTestContext();
});
afterEach(() => {
// Cleanup
testContext.cleanup();
});
describe('Happy Path', () => {
it('should handle valid input correctly', () => {
const result = FunctionToTest('valid-input');
expect(result).toBe('expected-output');
});
it('should process multiple items', () => {
const results = FunctionToTest(['item1', 'item2']);
expect(results).toHaveLength(2);
expect(results[0]).toBeDefined();
});
});
describe('Edge Cases', () => {
it('should handle empty input', () => {
const result = FunctionToTest('');
expect(result).toBe('');
});
it('should handle null/undefined', () => {
expect(() => FunctionToTest(null)).toThrow();
expect(() => FunctionToTest(undefined)).toThrow();
});
it('should handle maximum values', () => {
const largeInput = 'x'.repeat(10000);
const result = FunctionToTest(largeInput);
expect(result).toBeDefined();
});
});
describe('Error Handling', () => {
it('should throw on invalid input', () => {
expect(() => FunctionToTest('invalid')).toThrow('Invalid input');
});
it('should handle async errors gracefully', async () => {
await expect(
FunctionToTest('trigger-error')
).rejects.toThrow('Expected error');
});
});
});
```
## Test Coverage Goals
Create tests for:
-**Happy Path** (70%): Normal, expected usage
-**Edge Cases** (20%): Boundary conditions, empty values, max values
-**Error Cases** (10%): Invalid input, exceptions, failures
## Test Categories
### 1. Unit Tests
- Test individual functions in isolation
- Mock dependencies
- Fast execution
### 2. Integration Tests
- Test component interactions
- Use real dependencies where appropriate
- Verify data flow
### 3. API Tests
- Test endpoints with various payloads
- Verify status codes and responses
- Test authentication and authorization
### 4. UI Tests (if applicable)
- Test user interactions
- Verify rendering
- Test accessibility
## Output Format
```markdown
## Generated Test Suite
### 📁 Test Files Created
- `tests/unit/module.test.ts` (15 test cases)
- `tests/integration/api.test.ts` (8 test cases)
- `tests/e2e/user-flow.test.ts` (5 test cases)
### 📊 Coverage Summary
- **Total Test Cases**: 28
- **Happy Path**: 18 tests (64%)
- **Edge Cases**: 7 tests (25%)
- **Error Scenarios**: 3 tests (11%)
### 🎯 Coverage Areas
✅ User authentication flow
✅ Data validation
✅ API error handling
✅ Edge cases (null, empty, max values)
⚠️ Performance testing (manual review needed)
### ▶️ Run Tests
```bash
# Run all tests
npm test
# Run specific suite
npm test -- tests/unit/module.test.ts
# Run with coverage
npm test -- --coverage
```
### 📝 Manual Testing Checklist
- [ ] Performance with large datasets
- [ ] UI responsiveness on mobile
- [ ] Cross-browser compatibility
```
Use "Run Tests" handoff to execute and review results.
```
## Example 5: Migration Planner Agent
**Use Case**: Planning code migrations and refactoring
```markdown
---
description: Plan and guide large-scale code migrations and refactoring
name: migration-planner
tools: ['search', 'githubRepo', 'usages', 'fetch']
handoffs:
- label: Execute Migration
agent: agent
prompt: Execute the migration plan step by step with verification at each stage.
send: false
argument-hint: Describe the migration (e.g., "migrate from Vue 2 to Vue 3")
---
# Migration Planning Agent
You are a migration specialist who creates detailed, safe migration plans for large-scale code changes.
## Migration Planning Process
### 1. Assess Current State
- Use #tool:search to inventory existing code patterns
- Use #tool:githubRepo to understand project structure
- Use #tool:usages to find all usage locations
- Use #tool:fetch to research migration guides and best practices
### 2. Identify Impact
- List all files affected
- Identify breaking changes
- Map dependencies
- Estimate effort
### 3. Create Migration Plan
- Break into phases
- Define rollback strategy
- Plan testing approach
- Schedule timeline
### 4. Document Steps
- Detailed step-by-step instructions
- Verification steps
- Rollback procedures
## Migration Plan Template
```markdown
# Migration Plan: [From X to Y]
## Executive Summary
- **Scope**: [What's being migrated]
- **Impact**: [How many files/components affected]
- **Estimated Effort**: [Time estimate]
- **Risk Level**: [Low/Medium/High]
## Current State Analysis
### Inventory
- Total files using old pattern: [N]
- Key dependencies: [List]
- Breaking changes: [List]
### Usage Patterns Found
```typescript
// Pattern 1: [Found in X files]
// Pattern 2: [Found in Y files]
```
## Migration Strategy
### Phase 1: Preparation
1. [ ] Update dependencies
2. [ ] Create feature flags
3. [ ] Set up parallel testing
4. [ ] Document current behavior
### Phase 2: Incremental Migration
1. [ ] Migrate utility functions (5 files)
2. [ ] Migrate components (12 files)
3. [ ] Update tests (8 files)
4. [ ] Update documentation
### Phase 3: Validation
1. [ ] Run full test suite
2. [ ] Perform manual testing
3. [ ] Load testing
4. [ ] Security review
### Phase 4: Cleanup
1. [ ] Remove old code
2. [ ] Remove feature flags
3. [ ] Update dependencies
4. [ ] Archive documentation
## Detailed Steps
### Step 1: [Step Name]
**Files to modify**: `file1.ts`, `file2.ts`
**Changes**:
```typescript
// Before
oldPattern();
// After
newPattern();
```
**Verification**:
```bash
npm test -- file1.test.ts
```
## Rollback Plan
If issues occur at any phase:
1. Revert to commit: `[hash]`
2. Disable feature flag: `MIGRATION_ENABLED=false`
3. Restore from backup: `backup-[date]`
## Testing Strategy
### Automated Tests
- Unit tests for each migrated file
- Integration tests for workflows
- Regression tests for unchanged behavior
### Manual Tests
- [ ] User flow 1
- [ ] User flow 2
- [ ] Edge case scenarios
## Risk Mitigation
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| Breaking change in production | High | Low | Feature flags, gradual rollout |
| Performance degradation | Medium | Medium | Load testing before deployment |
| Data loss | High | Low | Database backups, dry runs |
## Timeline
- **Week 1**: Preparation and dependency updates
- **Week 2**: Phase 1 migration (utilities)
- **Week 3**: Phase 2 migration (components)
- **Week 4**: Testing and validation
- **Week 5**: Production deployment and monitoring
## Success Criteria
- [ ] All tests passing
- [ ] No performance degradation
- [ ] Zero production incidents
- [ ] Documentation updated
- [ ] Team trained on new patterns
```
## Output Deliverables
1. **Migration Plan Document** (as shown above)
2. **File-by-File Checklist** (detailed change list)
3. **Testing Scripts** (validation automation)
4. **Rollback Procedures** (emergency recovery)
5. **Team Communication** (stakeholder updates)
Use "Execute Migration" handoff when ready to begin implementation.
```
---
**Examples ready!** These demonstrate various agent types and configurations for different use cases.

View File

@@ -0,0 +1,391 @@
# GitHub Copilot Agent Templates
## Basic Agent Template
```markdown
---
description: [One sentence describing what this agent does]
name: [agent-name]
tools: ['tool1', 'tool2', 'tool3']
---
# [Agent Name] Instructions
You are a [role description]. Your primary responsibility is to [main purpose].
## Core Responsibilities
1. [Responsibility 1]
2. [Responsibility 2]
3. [Responsibility 3]
## Guidelines
- [Guideline 1]
- [Guideline 2]
- [Guideline 3]
## Tool Usage
- Use #tool:search to [purpose]
- Use #tool:fetch to [purpose]
- Use #tool:githubRepo to [purpose]
## Output Format
[Describe expected output structure]
## Constraints
- DO: [What to do]
- DON'T: [What to avoid]
```
## Planning Agent Template
```markdown
---
description: Generate implementation plans for features and tasks
name: planner
tools: ['fetch', 'search', 'githubRepo', 'usages']
handoffs:
- label: Implement Plan
agent: coder
prompt: Implement the plan outlined above.
send: false
---
# Feature Planning Agent
You are a technical planner who creates detailed implementation plans without making code changes.
## Your Role
Analyze requirements and create comprehensive implementation plans that include:
- Architecture decisions
- File changes needed
- Step-by-step implementation approach
- Potential risks and considerations
- Testing strategy
## Guidelines
- **NO CODE EDITS**: Only plan, never implement
- Use #tool:search to understand existing codebase patterns
- Use #tool:fetch to retrieve documentation and best practices
- Use #tool:githubRepo to analyze repository structure
- Use #tool:usages to find how similar features are implemented
## Output Format
```
## Implementation Plan
### Overview
[Brief summary]
### Architecture Changes
- [Change 1]
- [Change 2]
### Files to Modify
1. `path/to/file1.ts` - [What to change]
2. `path/to/file2.ts` - [What to change]
### Implementation Steps
1. [Step 1]
2. [Step 2]
3. [Step 3]
### Testing Approach
- [Test requirement 1]
- [Test requirement 2]
### Risks & Considerations
- [Risk 1]
- [Risk 2]
```
## Handoff
When ready, use the "Implement Plan" button to transition to the coder agent.
```
## Implementation Agent Template
```markdown
---
description: Implement code changes following the provided plan
name: coder
tools: ['search', 'files', 'usages']
handoffs:
- label: Review Code
agent: reviewer
prompt: Review the implementation for quality and best practices.
send: false
- label: Write Tests
agent: tester
prompt: Create comprehensive tests for the implemented changes.
send: false
---
# Implementation Agent
You are a software engineer who implements code changes following best practices and the provided plan.
## Your Role
Write high-quality, maintainable code that:
- Follows the implementation plan
- Adheres to project conventions
- Includes proper error handling
- Is well-documented with comments
## Guidelines
- Use #tool:search to understand existing code patterns
- Use #tool:files to create and modify files
- Use #tool:usages to ensure consistency with existing usage patterns
- Follow the project's coding standards and conventions
- Keep changes focused and atomic
## Implementation Process
1. **Understand Context**: Review the plan and existing code
2. **Implement Changes**: Make the necessary code modifications
3. **Add Documentation**: Include comments and docstrings
4. **Verify Consistency**: Ensure changes align with codebase patterns
## Output Format
For each file modified:
```
✅ Modified: `path/to/file.ts`
- [Change description 1]
- [Change description 2]
```
## Handoffs
After implementation:
- **Review Code**: Have code reviewed for quality
- **Write Tests**: Create tests for the implementation
```
## Review Agent Template
```markdown
---
description: Review code for quality, security, and best practices
name: reviewer
tools: ['search', 'githubRepo', 'usages']
handoffs:
- label: Fix Issues
agent: coder
prompt: Address the issues identified in the review.
send: false
---
# Code Review Agent
You are a senior code reviewer who ensures code quality, security, and adherence to best practices.
## Your Role
Perform comprehensive code reviews checking for:
- **Security**: Vulnerabilities and security best practices
- **Quality**: Code maintainability and readability
- **Performance**: Potential performance issues
- **Best Practices**: Adherence to patterns and conventions
- **Testing**: Test coverage and quality
## Guidelines
- Use #tool:search to compare against project conventions
- Use #tool:githubRepo to understand repository standards
- Use #tool:usages to verify consistent usage patterns
- Be constructive and specific in feedback
- Prioritize issues by severity (Critical, High, Medium, Low)
## Review Checklist
- [ ] No security vulnerabilities (XSS, SQL injection, etc.)
- [ ] Proper error handling
- [ ] Code follows project conventions
- [ ] No performance bottlenecks
- [ ] Adequate test coverage
- [ ] Clear documentation and comments
- [ ] No code duplication
- [ ] Consistent naming conventions
## Output Format
```
## Code Review Report
### ✅ Strengths
- [Positive aspect 1]
- [Positive aspect 2]
### 🔴 Critical Issues
- [ ] `file.ts:42` - [Issue description and fix]
### 🟡 Suggestions
- [ ] `file.ts:67` - [Suggestion description]
### 📊 Summary
- Security: ✅ No issues found
- Quality: ⚠️ 2 suggestions
- Performance: ✅ Looks good
- Testing: ❌ Needs improvement
### Next Steps
[Recommended actions]
```
## Handoff
Use "Fix Issues" button to send critical issues back to implementation.
```
## Testing Agent Template
```markdown
---
description: Create comprehensive tests for code changes
name: tester
tools: ['search', 'files', 'usages']
handoffs:
- label: Review Tests
agent: reviewer
prompt: Review the test coverage and quality.
send: false
---
# Testing Agent
You are a testing specialist who creates comprehensive, maintainable test suites.
## Your Role
Create high-quality tests that:
- Cover all critical paths and edge cases
- Are maintainable and readable
- Follow testing best practices
- Provide meaningful assertions
## Guidelines
- Use #tool:search to find existing test patterns
- Use #tool:files to create test files
- Use #tool:usages to understand how code is used in practice
- Follow the project's testing framework conventions
- Aim for meaningful coverage, not just high percentages
## Test Types to Consider
1. **Unit Tests**: Individual function/method behavior
2. **Integration Tests**: Component interactions
3. **Edge Cases**: Boundary conditions and error scenarios
4. **Regression Tests**: Known bug scenarios
## Output Format
```
## Test Suite Created
### 📝 Test Files
- `tests/feature.test.ts` - [Description]
- `tests/integration.test.ts` - [Description]
### ✅ Coverage
- Unit tests: [X] test cases
- Integration tests: [Y] scenarios
- Edge cases: [Z] scenarios
### 🎯 Test Summary
- Total test cases: [N]
- Coverage areas: [List key areas]
- Known gaps: [If any]
### Run Tests
```bash
npm test [test-file-pattern]
```
```
## Handoff
Use "Review Tests" to have test quality reviewed.
```
## Documentation Agent Template
```markdown
---
description: Generate and update technical documentation
name: documenter
tools: ['search', 'files', 'githubRepo']
---
# Documentation Agent
You are a technical writer who creates clear, comprehensive documentation.
## Your Role
Create documentation that is:
- Clear and accessible to the target audience
- Comprehensive with examples
- Up-to-date with current implementation
- Well-structured and easy to navigate
## Guidelines
- Use #tool:search to understand code functionality
- Use #tool:files to create/update documentation files
- Use #tool:githubRepo to understand project structure
- Include code examples and usage patterns
- Follow the project's documentation style
## Documentation Types
1. **API Documentation**: Endpoints, parameters, responses
2. **User Guides**: How to use features
3. **Developer Guides**: How to contribute/extend
4. **README**: Project overview and quick start
## Output Format
```markdown
# [Feature/Module Name]
## Overview
[Brief description]
## Installation
```bash
[Installation commands]
```
## Usage
```[language]
[Code example]
```
## API Reference
### [Function/Method Name]
- **Parameters**: [List]
- **Returns**: [Type and description]
- **Example**: [Code example]
## Examples
[Comprehensive examples]
## Troubleshooting
[Common issues and solutions]
```
---
**Templates ready for agent generation!** Use these as starting points and customize for specific needs.

109
skills/instruments/SKILL.md Normal file
View File

@@ -0,0 +1,109 @@
---
name: ios-instruments-performance-cli
description: Use Xcode Instruments command line tools to analyze iOS app performance, detect memory leaks, optimize launch times, monitor CPU usage, and identify performance bottlenecks for the iOS project
---
# iOS Instruments Performance CLI
## Instructions
When helping with iOS app performance optimization using Instruments command line tools:
### 1. Setup Analysis Environment
**CRITICAL: Always use device UUID, never device names**
- Get device UUID: `xcrun simctl list devices available | grep "iPhone"`
- Device names like "iPhone 17 Pro" are ambiguous and will fail
- Use UUID format: `F464E766-555C-4B95-B8CC-763702A70791`
**Clean installation state (REQUIRED)**
- Always uninstall app before profiling: `xcrun simctl uninstall $DEVICE_UUID <bundle id>`
- Multiple app installations cause "process is ambiguous" errors
- Alternative: Completely erase simulator for cleanest state
**Build configuration**
- Build in Release mode for accurate performance measurements
### 2. Choose Appropriate Instrument Template
Use `xcrun xctrace list templates` to see available templates:
- **App Launch** - Essential for launch time optimization (most common)
- **Time Profiler** - CPU performance analysis
- **Allocations** - Memory usage tracking
- **Leaks** - Memory leak detection
- **Network** - API calls and network activity
- **Animation Hitches** - UI performance issues
### 3. Run Performance Analysis
**Standard workflow:**
```bash
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # this is sample uuid, run command line to get exist uuid
xcrun simctl uninstall $DEVICE_UUID <bundle id>
xcrun simctl install $DEVICE_UUID /path/to/<app name>.app
sleep 2
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
--launch -- /path/to/<app name>.app 2>&1
```
**Important notes:**
- Trace files auto-generate names like `Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace`
- `--output` parameter may be ignored; accept auto-generated names
- Wait 2 seconds after installation before profiling
- Use `2>&1` to capture all output
- Recording duration: 10-30s for launch, 2-5m for other analyses
### 4. Analyze Results
**Finding trace files:**
```bash
ls -lt *.trace | head -1 # Most recent trace
find . -name "*.trace" -type d -mmin -5 # Files from last 5 minutes
```
**Analysis approaches:**
- **CLI export often fails** - "trace is malformed" errors are common
- **Recommended**: Open in Instruments.app GUI for reliable analysis
```bash
open -a Instruments.app Launch_<app name>*.trace
```
- Parse signpost markers for performance metrics
- Identify bottlenecks in launch sequence, memory allocations, CPU hotspots
### 5. Common Optimization Patterns
Based on successful optimizations:
- **Lazy initialization** - Defer expensive dependency resolution
- **Background operations** - Move non-critical setup off main thread
- **Deferred bindings** - Set up subscriptions after UI appears
- **Font loading** - Register fonts asynchronously
- **Method swizzling** - Only essential swizzles during launch
### Critical Pitfalls to Avoid
❌ Using device names instead of UUIDs
❌ Skipping clean installation step
❌ Building in Debug mode
❌ Trying to export trace immediately after recording
❌ Assuming --output path will be used
❌ Not waiting between installation and profiling
### Reference Documentation
See [examples](./examples.md) in this skill directory for:
- Detailed command examples with correct syntax
- Complete troubleshooting guide with solutions
- Real-world lessons learned
- Production-ready workflow scripts

View File

@@ -0,0 +1,687 @@
# XCTrace & Instruments Examples for iOS project
## Quick Reference - Working Commands ✅
These commands have been tested and verified to work correctly:
```bash
# Get device UUID (always required)
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # iPhone 17 Pro
xcrun simctl list devices available | grep "iPhone"
# App Launch Profiling (most common use case)
xcrun simctl uninstall $DEVICE_UUID <bundle id>
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
sleep 2
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app 2>&1
# Find generated trace file
ls -lt *.trace | head -1
# Open in Instruments for analysis
open -a Instruments.app Launch_<app name>*.trace
```
## XCTrace Overview
`xcrun xctrace` is the modern command-line interface for Instruments profiling. It replaces the deprecated `instruments` command and provides more reliable automation capabilities.
## Available Templates
Run `xcrun xctrace list templates` to see all available templates:
### Standard Templates
- **Activity Monitor**: General system activity monitoring
- **Allocations**: Memory allocation tracking
- **Animation Hitches**: UI animation performance issues
- **App Launch**: App startup performance analysis
- **Audio System Trace**: Audio subsystem performance
- **CPU Counters**: Hardware performance counters
- **CPU Profiler**: CPU usage profiling
- **Core ML**: Machine learning model performance
- **Data Persistence**: Core Data and file I/O analysis
- **File Activity**: File system operations
- **Game Memory**: Game-specific memory analysis
- **Game Performance**: Game-specific performance metrics
- **Game Performance Overview**: High-level game metrics
- **Leaks**: Memory leak detection
- **Logging**: System and app logging analysis
- **Metal System Trace**: GPU performance analysis
- **Network**: Network activity monitoring
- **Power Profiler**: Energy consumption analysis
- **Processor Trace**: Low-level CPU tracing
- **RealityKit Trace**: AR/VR performance analysis
- **Swift Concurrency**: Swift async/await performance
- **SwiftUI**: SwiftUI-specific performance analysis
- **System Trace**: System-wide performance analysis
- **Tailspin**: System responsiveness analysis
- **Time Profiler**: CPU time profiling
## Device Management
### List Available Devices
```bash
# List all devices (physical and simulators)
xcrun xctrace list devices
# List only simulators with UUIDs (RECOMMENDED)
xcrun simctl list devices available | grep "iPhone"
```
### Current Available Devices for <app scheme>
- **Simulators**: iPhone 16 Pro, iPhone 16, iPhone 17 Pro (F464E766-555C-4B95-B8CC-763702A70791), iPad variants, etc.
### ⚠️ IMPORTANT: Always Use Device UUID, Not Name
Device names can be ambiguous. **Always use the device UUID** for reliable automation:
```bash
# ❌ WRONG - Ambiguous device name
xcrun xctrace record --template "App Launch" --device "iPhone 16 Pro" ...
# ✅ CORRECT - Use UUID
xcrun xctrace record --template "App Launch" --device F464E766-555C-4B95-B8CC-763702A70791 ...
```
## Basic XCTrace Commands
### 1. App Launch Performance Analysis
```bash
# ✅ CORRECT METHOD - Clean state, use UUID, proper app path handling
# Step 1: Get device UUID
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # iPhone 17 Pro
# Step 2: Clean up any existing app installations (IMPORTANT to avoid ambiguity)
xcrun simctl uninstall $DEVICE_UUID <bundle id>
# Step 3: Install fresh app build
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
# Step 4: Wait for installation to complete
sleep 2
# Step 5: Profile app launch
xcrun xctrace record --template "App Launch" \
--device $DEVICE_UUID \
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
--output ~/Desktop/<app name>_launch_analysis.trace 2>&1
# Note: Trace file will be saved with auto-generated name in current directory
# Example: Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace
```
**Alternative: Complete Clean State (Most Reliable)**
```bash
# For most reliable results, completely erase and reboot simulator
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
xcrun simctl shutdown $DEVICE_UUID
xcrun simctl erase $DEVICE_UUID
xcrun simctl boot $DEVICE_UUID
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
sleep 2
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
2>&1
```
### 2. Memory Allocation Tracking
```bash
# ✅ Track memory allocations during app usage
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
# Option 1: Attach to running app
xcrun xctrace record --template "Allocations" \
--device $DEVICE_UUID \
--attach "<app name>" \
--time-limit 5m
# Option 2: Launch and track from start
xcrun xctrace record --template "Allocations" \
--device $DEVICE_UUID \
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
--time-limit 5m
```
### 3. CPU Time Profiling
```bash
# ✅ Profile CPU usage
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
xcrun xctrace record --template "Time Profiler" \
--device $DEVICE_UUID \
--attach "<app name>" \
--time-limit 2m
```
### 4. Memory Leak Detection
```bash
# ✅ Detect memory leaks
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
xcrun xctrace record --template "Leaks" \
--device $DEVICE_UUID \
--attach "<app name>" \
--time-limit 3m
```
### 5. Network Activity Monitoring
```bash
# Monitor network requests and responses
xcrun xctrace record --template "Network" \
--device "iPhone 16 Pro Simulator (18.5)" \
--attach "<app name>" \
--output ~/Desktop/<app name>_network_analysis.trace \
--time-limit 5m
```
### 6. SwiftUI Performance Analysis
```bash
# Analyze SwiftUI performance (if using SwiftUI)
xcrun xctrace record --template "SwiftUI" \
--device "iPhone 16 Pro Simulator (18.5)" \
--attach "<app name>" \
--output ~/Desktop/<app name>_swiftui_analysis.trace \
--time-limit 2m
```
### 7. Animation Performance
```bash
# Detect animation hitches and performance issues
xcrun xctrace record --template "Animation Hitches" \
--device "iPhone 16 Pro Simulator (18.5)" \
--attach "<app name>" \
--output ~/Desktop/<app name>_animation_analysis.trace \
--time-limit 3m
```
### 8. Power/Energy Analysis
```bash
# Analyze energy consumption
xcrun xctrace record --template "Power Profiler" \
--device "iPhone 16 Pro Simulator (18.5)" \
--attach "<app name>" \
--output ~/Desktop/<app name>_power_analysis.trace \
--time-limit 10m
```
## Advanced Usage
### Recording All Processes
```bash
# Record system-wide performance
xcrun xctrace record --template "System Trace" \
--device "iPhone 16 Pro Simulator (18.5)" \
--all-processes \
--output ~/Desktop/<app name>_system_trace.trace \
--time-limit 1m
```
### Custom Environment Variables
```bash
# Launch with custom environment variables
xcrun xctrace record --template "Time Profiler" \
--device "iPhone 16 Pro Simulator (18.5)" \
--env "LOG_LEVEL=verbose" \
--launch -- "/path/to/<app name>.app" \
--output ~/Desktop/<app name>_debug_profile.trace \
--time-limit 2m
```
### Multiple Instruments
```bash
# Combine multiple instruments in one session
xcrun xctrace record \
--device "iPhone 16 Pro Simulator (18.5)" \
--instrument "Time Profiler" \
--instrument "Allocations" \
--instrument "Network" \
--attach "<app name>" \
--output ~/Desktop/<app name>_combined_analysis.trace \
--time-limit 3m
```
## Data Export and Analysis
### Table of Contents Export
```bash
# View available data in trace file
xcrun xctrace export --input ~/Desktop/<app name>_launch_analysis.trace --toc
```
### Specific Data Export
```bash
# Export CPU profiling data
xcrun xctrace export --input ~/Desktop/<app name>_cpu_profile.trace \
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profiler"]' \
--output ~/Desktop/cpu_data.xml
# Export memory allocation data
xcrun xctrace export --input ~/Desktop/<app name>_memory_analysis.trace \
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="allocations"]' \
--output ~/Desktop/memory_data.xml
# Export network data as HAR file
xcrun xctrace export --input ~/Desktop/<app name>_network_analysis.trace \
--har --output ~/Desktop/network_data.har
```
## iOS Project Specific Workflows
### Complete Performance Audit Script
```bash
#!/bin/bash
# <app name>_performance_audit.sh
# Configuration
DEVICE="iPhone 16 Pro Simulator (18.5)"
APP_PATH="/Users/daipham/Library/Developer/Xcode/DerivedData/<app name>-demupeapxadrllglwxuahiesemhe/Build/Products/Release-iphonesimulator/<app name>.app"
OUTPUT_DIR="~/Desktop/<app name>_performance_$(date +%Y%m%d_%H%M%S)"
BUNDLE_ID="<bundle id>"
# Create output directory
mkdir -p "$OUTPUT_DIR"
echo "🚀 Starting <app scheme> Performance Audit..."
echo "📁 Results will be saved to: $OUTPUT_DIR"
# 1. App Launch Analysis
echo "📱 Analyzing app launch performance..."
xcrun xctrace record --template "App Launch" \
--device "$DEVICE" \
--launch -- "$APP_PATH" \
--output "$OUTPUT_DIR/launch_analysis.trace" \
--time-limit 30s
# 2. Memory Allocations
echo "🧠 Analyzing memory allocations..."
xcrun xctrace record --template "Allocations" \
--device "$DEVICE" \
--attach "$BUNDLE_ID" \
--output "$OUTPUT_DIR/memory_analysis.trace" \
--time-limit 2m
# 3. CPU Profiling
echo "⚡ Profiling CPU usage..."
xcrun xctrace record --template "Time Profiler" \
--device "$DEVICE" \
--attach "$BUNDLE_ID" \
--output "$OUTPUT_DIR/cpu_profile.trace" \
--time-limit 2m
# 4. Memory Leaks
echo "🔍 Checking for memory leaks..."
xcrun xctrace record --template "Leaks" \
--device "$DEVICE" \
--attach "$BUNDLE_ID" \
--output "$OUTPUT_DIR/leaks_analysis.trace" \
--time-limit 3m
# 5. Network Activity
echo "🌐 Monitoring network activity..."
xcrun xctrace record --template "Network" \
--device "$DEVICE" \
--attach "$BUNDLE_ID" \
--output "$OUTPUT_DIR/network_analysis.trace" \
--time-limit 3m
# 6. Animation Performance
echo "🎬 Analyzing animation performance..."
xcrun xctrace record --template "Animation Hitches" \
--device "$DEVICE" \
--attach "$BUNDLE_ID" \
--output "$OUTPUT_DIR/animation_analysis.trace" \
--time-limit 2m
echo "✅ Performance audit complete!"
echo "📂 Open traces in Instruments.app for detailed analysis"
echo "🔗 Trace files location: $OUTPUT_DIR"
```
### Continuous Integration Performance Testing
```bash
#!/bin/bash
# ci_performance_check.sh - For automated CI/CD pipelines
DEVICE="iPhone 16 Pro Simulator (18.5)"
APP_PATH="$1" # Pass app path as parameter
THRESHOLD_LAUNCH_TIME=3.0 # seconds
THRESHOLD_MEMORY_MB=150 # MB
# Quick launch time check
echo "⏱️ Measuring app launch time..."
xcrun xctrace record --template "App Launch" \
--device "$DEVICE" \
--launch -- "$APP_PATH" \
--output "launch_check.trace" \
--time-limit 10s
# Extract launch time (simplified - would need proper parsing)
# LAUNCH_TIME=$(parse_launch_time launch_check.trace)
# Memory footprint check
echo "💾 Measuring memory footprint..."
xcrun xctrace record --template "Allocations" \
--device "$DEVICE" \
--launch -- "$APP_PATH" \
--output "memory_check.trace" \
--time-limit 30s
echo "📊 Performance check complete"
# Add logic to fail CI if thresholds exceeded
```
### Development Workflow Integration
```bash
# Quick development profiling
alias <app name>_profile='xcrun xctrace record --template "Time Profiler" --device "iPhone 16 Pro Simulator (18.5)" --attach "<app name>" --output ~/Desktop/quick_profile_$(date +%H%M%S).trace --time-limit 1m'
alias <app name>_memory='xcrun xctrace record --template "Allocations" --device "iPhone 16 Pro Simulator (18.5)" --attach "<app name>" --output ~/Desktop/memory_check_$(date +%H%M%S).trace --time-limit 2m'
alias <app name>_launch='xcrun xctrace record --template "App Launch" --device "iPhone 16 Pro Simulator (18.5)" --launch -- "/Users/daipham/Library/Developer/Xcode/DerivedData/<app name>-demupeapxadrllglwxuahiesemhe/Build/Products/Release-iphonesimulator/<app name>.app" --output ~/Desktop/launch_$(date +%H%M%S).trace --time-limit 20s'
```
## Tips and Best Practices
### Device Selection
- Use physical devices for accurate performance data
- Simulators are good for development and debugging
- Match device to your target audience (iPhone vs iPad)
### Template Selection
- **App Launch**: Essential for user experience optimization
- **Time Profiler**: First choice for CPU performance issues
- **Allocations**: Critical for memory management
- **Leaks**: Important for long-term app stability
- **Network**: Essential for apps with API calls (like <app scheme>)
### Recording Duration
- App Launch: 15-30 seconds
- Memory Analysis: 2-5 minutes
- CPU Profiling: 1-3 minutes
- Leak Detection: 3-10 minutes (longer for thorough analysis)
### Automation Best Practices
- Always specify output paths to avoid conflicts
- Use timestamp-based file naming
- Set appropriate time limits to prevent infinite recording
- Consider device state (clean vs. with existing apps)
## Common XPath Expressions for Data Export
```bash
# CPU profiling data
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profiler"]'
# Memory allocations
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="allocations"]'
# Network requests
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="network-connections"]'
# App launch metrics
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="app-launch"]'
# Animation hitches
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="animation-hitches"]'
```
## Troubleshooting
### Common Issues and Solutions
#### 1. ❌ "Provided device parameter is ambiguous"
**Problem:**
```bash
xcrun xctrace record --template "App Launch" --device "iPhone 17 Pro" ...
# Error: Provided device parameter 'iPhone 17 Pro' is ambiguous
```
**Solution:** Always use device UUID instead of name
```bash
# Get UUID first
xcrun simctl list devices available | grep "iPhone 17 Pro"
# Output: iPhone 17 Pro (F464E766-555C-4B95-B8CC-763702A70791) (Booted)
# Use UUID in command
xcrun xctrace record --template "App Launch" --device F464E766-555C-4B95-B8CC-763702A70791 ....
```
#### 2. ❌ "Provided process is ambiguous"
**Problem:**
```bash
# Error: Provided process '/path/to/<app name>.app' is ambiguous
# /path1/<app name>.app
# /path2/<app name>.app
```
**Root Cause:** Multiple app installations exist on the simulator (common after repeated builds)
**Solution:** Clean up before profiling
```bash
# Method 1: Uninstall existing app
xcrun simctl uninstall $DEVICE_UUID <bundle id>
# Method 2: Erase entire simulator (nuclear option)
xcrun simctl shutdown $DEVICE_UUID
xcrun simctl erase $DEVICE_UUID
xcrun simctl boot $DEVICE_UUID
# Then reinstall fresh
xcrun simctl install $DEVICE_UUID /path/to/<app name>.app
```
#### 3. ❌ "Export failed: Trace is malformed - run data is missing"
**Problem:**
```bash
xcrun xctrace export --input trace.trace --toc
# Error: Export failed: Trace is malformed - run data is missing
```
**Root Cause:** Trace file is still being written or profiling was interrupted
**Solution:**
- Wait for profiling to fully complete (look for "Output file saved as:" message)
- Don't try to export immediately after xctrace record completes
- Some traces may not export properly via CLI - open in Instruments.app instead
```bash
# Check if profiling completed
ls -la *.trace # Look for complete directory structure
# Open in Instruments for analysis instead
open -a Instruments.app trace.trace
```
#### 4. ❌ "No such file or directory" with paths containing spaces
**Problem:**
```bash
xcrun xctrace record --input "/path with spaces/file.trace" ...
# Error: File does not exist at path
```
**Solution:** Use environment variables or proper quoting
```bash
# Method 1: Environment variable (RECOMMENDED)
TRACE_FILE="/path with spaces/file.trace"
xcrun xctrace export --input "$TRACE_FILE" --toc
# Method 2: Escape properly
xcrun xctrace export --input "/path\ with\ spaces/file.trace" --toc
```
#### 5. ❌ Trace file saved in wrong location
**Problem:** Specified `--output ~/Desktop/trace.trace` but file appears in current directory
**Root Cause:** xctrace may ignore --output path and auto-generate filename
**Solution:**
```bash
# Trace files are saved with auto-generated names like:
# Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace
# Find your trace file
find . -name "*.trace" -type d -mmin -5 # Files modified in last 5 minutes
# Or look for specific pattern
ls -lt *.trace | head -1 # Most recent trace
```
#### 6. ❌ App Launch template not capturing data
**Problem:** Trace file created but no meaningful data
**Solution:**
- Ensure app actually launches (check simulator)
- Build app in Release configuration for realistic performance
- Don't specify --time-limit too short (use at least 10-15 seconds)
- Check that device is booted before profiling
```bash
# Verify device is booted
xcrun simctl list devices | grep Booted
# Boot if needed
xcrun simctl boot $DEVICE_UUID
```
### Debugging Commands
```bash
# List all available devices with UUIDs
xcrun simctl list devices available
# Check if app is installed
xcrun simctl listapps $DEVICE_UUID | grep <app name>
# Verbose output
xcrun xctrace record --template "Time Profiler" --device "$DEVICE_UUID" --time-limit 30s --verbose
# Check available instruments
xcrun xctrace list instruments
# Validate trace file structure
ls -la trace.trace/
# Open trace in Instruments GUI (most reliable for analysis)
open -a Instruments.app trace.trace
```
### Best Practices to Avoid Issues
1. **Always use device UUID** - Never use device names
2. **Clean state before profiling** - Uninstall app or erase simulator first
3. **Use environment variables** - For paths with spaces or complex names
4. **Wait between steps** - Add `sleep 2` after installation before profiling
5. **Build in Release mode** - For accurate performance measurements
6. **Check output location** - Trace files may have auto-generated names
7. **Use Instruments.app** - For final analysis when CLI export fails
---
## Lessons Learned from Real-World Usage (2025-10-30)
### What Worked ✅
1. **Device UUID approach**: Using `xcrun simctl list devices available | grep "iPhone"` to get UUID, then using UUID in all commands
2. **Clean installation**: Running `xcrun simctl uninstall` before each profiling session eliminated ambiguity errors
3. **Environment variables**: Using `DEVICE_UUID="..."` made commands more reliable with special characters
4. **Background execution**: Using `2>&1` redirect and `run_in_background` for long-running profiles
5. **Auto-generated filenames**: Accepting that xctrace generates its own filenames (e.g., `Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace`)
### What Didn't Work ❌
1. **Device names**: `--device "iPhone 17 Pro"` or `--device "iPhone 16 Pro Simulator (18.5)"` - Always ambiguous
2. **Direct export**: `xcrun xctrace export --input trace.trace --toc` often failed with "malformed trace" errors
3. **Absolute --output paths**: Often ignored by xctrace, files saved with auto-generated names instead
4. **Multiple app installations**: Caused "process is ambiguous" errors - must clean up first
5. **Immediate export**: Trying to export trace immediately after recording before file fully written
### Performance Improvements Achieved
From this profiling session, we identified and fixed these launch time issues in <app name> app:
1. **Lazy ViewModel initialization**: Deferred 7 use case resolutions from eager to lazy loading
2. **Deferred swizzling**: Moved 4 tracking swizzle operations to background thread
3. **Background font loading**: Moved `FontFamily.registerAllCustomFonts()` off main thread
4. **Deferred RxSwift bindings**: Moved subscription setup to next run loop iteration
**Expected improvement**: 40-60% reduction in main thread blocking during launch
### Recommended Workflow
```bash
#!/bin/bash
# Proven workflow for app launch profiling
# 1. Setup
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
APP_PATH="/Users/<username>/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app"
# 2. Build in Release
xcodebuild -workspace <app name>.xcworkspace \
-scheme "<app scheme>" \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath ~/Library/Developer/Xcode/DerivedData/<app name> \
build
# 3. Clean simulator state
xcrun simctl uninstall $DEVICE_UUID <bundle id>
# 4. Install fresh
xcrun simctl install $DEVICE_UUID "$APP_PATH"
sleep 2
# 5. Profile
xcrun xctrace record --template "App Launch" \
--device $DEVICE_UUID \
--launch -- "$APP_PATH" 2>&1
# 6. Find and open trace
TRACE=$(ls -t Launch_<app name>*.trace 2>/dev/null | head -1)
echo "Trace saved to: $TRACE"
open -a Instruments.app "$TRACE"
```
This workflow has been tested and verified to work reliably.

View File

@@ -0,0 +1,157 @@
---
name: ios-code-review
description: Comprehensive iOS Swift code review for Payoo Merchant app. Checks RxSwift patterns, Clean Architecture, naming conventions, memory management, security, and performance. Use when reviewing Swift files, pull requests, or ViewModels, ViewControllers, UseCases, and Repositories.
allowed-tools: Read, Grep, Glob
---
# iOS Code Review
Expert iOS code reviewer for Payoo Merchant application, specializing in Swift, RxSwift reactive programming, and Clean Architecture patterns.
## When to Activate
- "review code", "check this file", "review PR"
- Mentions Swift files: ViewController, ViewModel, UseCase, Repository
- "code quality", "best practices", "check standards"
- RxSwift, Clean Architecture, MVVM patterns
## Review Process
### Step 1: Identify Scope
Determine what to review:
- Specific files (e.g., "PaymentViewModel.swift")
- Directories (e.g., "Payment module")
- Git changes (recent commits, PR diff)
- Entire module or feature
### Step 2: Read and Analyze
Use Read tool to examine files, checking against 6 core categories.
### Step 3: Apply Standards
#### 1. Naming Conventions ✅
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
- **No abbreviations** except URL, ID, VC, UC
- **IBOutlets**: Include type suffix (e.g., `amountTextField`)
#### 2. RxSwift Patterns 🔄
- **Disposal**: Every `.subscribe()` has `.disposed(by: disposeBag)`
- **Memory**: Use `[weak self]` in closures
- **Schedulers**: `subscribeOn(background)` for work, `observeOn(main)` for UI
- **Errors**: All chains handle errors
- **Relays**: Use `BehaviorRelay` not `BehaviorSubject`
#### 3. Clean Architecture 🏗️
- **Flow**: ViewModel → UseCase → Repository → API/DB
- **ViewModels**: Extend `BaseViewModel<State>`, no business logic
- **UseCases**: Contain all business logic
- **DI**: Dependencies injected via constructor (Swinject)
#### 4. Security 🔒
- Payment data in Keychain, never UserDefaults
- No sensitive data in logs
- HTTPS with certificate pinning
- Input validation on amounts
#### 5. UI/UX 🎨
- Simple titles: Use `title` property
- Complex titles: Use `navigationItem.titleView` only when subtitle exists
- Accessibility labels and hints
- Loading states with feedback
#### 6. Performance ⚡
- Database ops on background threads
- No retain cycles
- Image caching
- Proper memory management
### Step 4: Generate Report
Provide structured output with:
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
- **Issues by category**: Organized findings
- **Code examples**: Current vs. fixed code
- **Explanations**: Why it matters
- **Recommendations**: Prioritized actions
## Severity Levels
🔴 **Critical** - Fix immediately
- Missing `.disposed(by: disposeBag)` → Memory leak
- Strong `self` references → Retain cycle
- Payment data in UserDefaults → Security risk
- UI updates off main thread → Crash risk
🟠 **High Priority** - Fix soon
- No error handling in Observable chains
- Wrong scheduler usage
- ViewModel calling API directly
- Business logic in ViewModel
🟡 **Medium Priority** - Should improve
- Using deprecated `BehaviorSubject`
- Poor naming (abbreviations)
- Missing accessibility labels
🟢 **Low Priority** - Nice to have
- Inconsistent style
- Could be more descriptive
## Output Format
```markdown
# iOS Code Review Report
## Summary
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
- By category: Naming: X, RxSwift: X, Architecture: X, etc.
## Critical Issues
### 🔴 [Category] - [Issue Title]
**File**: `path/to/file.swift:line`
**Current**:
```swift
// problematic code
```
**Fix**:
```swift
// corrected code
```
**Why**: [Explanation of impact]
---
## Recommendations
1. Fix all critical issues immediately
2. Address high priority before next release
3. Plan medium priority for next sprint
## Positive Observations
✅ [Acknowledge well-written code]
```
## Quick Reference
**Standards**: `.github/instructions/ios-merchant-code-review.instructions.md`
- Lines 36-393: Naming Conventions
- Lines 410-613: RxSwift Patterns
- Lines 615-787: Architecture
- Lines 789-898: Security
- Lines 1181-1288: Testing
- Lines 1363-1428: Performance
**Detailed Examples**: See `examples.md` in this skill directory for extensive code examples and patterns.
## Tips
- **Be thorough**: Check all 6 categories
- **Be specific**: Reference exact line numbers
- **Be constructive**: Explain why, not just what
- **Be practical**: Prioritize by severity
- **Be encouraging**: Acknowledge good code

View File

@@ -0,0 +1,637 @@
# iOS Code Review Examples
Detailed examples for each review category with good/bad patterns.
## 1. Naming Conventions Examples
### ✅ Good Naming
```swift
// Classes and Types
class PaymentViewController: UIViewController { }
class RefundRequestViewModel: BaseViewModel<RefundRequestState> { }
protocol PaymentUseCase { }
// Variables and Properties
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
let transactions = BehaviorRelay<[Transaction]>(value: [])
// Functions
func loadTransactions() { }
func processPaymentRequest(amount: Double) { }
func validatePaymentAmount(_ amount: String) -> Bool { }
// IBOutlets
@IBOutlet weak var paymentAmountTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton!
@IBOutlet weak var transactionTableView: UITableView!
```
### ❌ Bad Naming
```swift
// Classes - Too abbreviated or generic
class PayVC: UIViewController { } // What is "Pay"?
class RefReqVM { } // Too abbreviated
class Manager { } // Too generic
// Variables - Unclear or abbreviated
let amt = BehaviorRelay<String>(value: "") // What is "amt"?
let flag = BehaviorRelay<Bool>(value: false) // Meaningless
let data = BehaviorRelay<[Any]>(value: []) // Too generic
// Functions - Vague
func doSomething() { } // What does it do?
func process() { } // Process what?
func handle() { } // Handle what?
// IBOutlets - Missing type suffix
@IBOutlet weak var amount: UITextField! // Should be amountTextField
@IBOutlet weak var btn: UIButton! // Should be confirmButton
```
---
## 2. RxSwift Pattern Examples
### ✅ Proper RxSwift Usage
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessing = BehaviorRelay<Bool>(value: false)
init(paymentUC: PaymentUseCase) {
self.paymentUC = paymentUC
super.init()
}
func processPayment() {
guard !paymentAmount.value.isEmpty else {
setState(.showError(PaymentError.invalidAmount))
return
}
isProcessing.accept(true)
paymentUC.execute(amount: paymentAmount.value)
.subscribeOn(ConcurrentScheduler.background)
.observeOn(MainScheduler.instance)
.subscribe(
onNext: { [weak self] result in
self?.handleSuccess(result)
},
onError: { [weak self] error in
self?.handleError(error)
},
onCompleted: { [weak self] in
self?.isProcessing.accept(false)
}
)
.disposed(by: disposeBag) // Proper disposal
}
}
```
### ❌ Common RxSwift Mistakes
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
// Missing DisposeBag property
func processPayment() {
// MEMORY LEAK: No disposal
paymentUC.execute(amount: paymentAmount.value)
.subscribe(onNext: { result in
// RETAIN CYCLE: Strong self reference
self.handleSuccess(result)
})
// MISSING: .disposed(by: disposeBag)
}
func loadData() {
// Wrong scheduler for UI updates
networkService.fetchData()
.subscribeOn(MainScheduler.instance) // Wrong!
.subscribe(onNext: { data in
self.tableView.reloadData() // May be on background thread
})
.disposed(by: disposeBag)
}
func refreshData() {
// No error handling
dataSource.getData()
.subscribe(onNext: { data in
// Handle data
})
// MISSING: onError handler
.disposed(by: disposeBag)
}
}
```
### DisposeBag Anti-Patterns
```swift
// BAD: Local DisposeBag
func loadData() {
let disposeBag = DisposeBag() // Local variable!
api.fetchData()
.subscribe(onNext: { data in
// Handle data
})
.disposed(by: disposeBag)
// DisposeBag deallocates here, cancels subscription immediately!
}
// BAD: Multiple DisposeBags
class ViewModel {
private var searchDisposeBag = DisposeBag() // Anti-pattern
private var dataDisposeBag = DisposeBag() // Anti-pattern
}
// GOOD: Single DisposeBag property
class ViewModel {
private let disposeBag = DisposeBag() // Correct!
}
```
---
## 3. Clean Architecture Examples
### ✅ Proper Layer Separation
```swift
// PRESENTATION LAYER - ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase // Uses UseCase
init(paymentUC: PaymentUseCase) {
self.paymentUC = paymentUC
super.init()
}
func processPayment(amount: String) {
paymentUC.execute(amount: amount) // Delegates to UseCase
.subscribe(
onNext: { [weak self] result in
self?.setState(.success(result))
},
onError: { [weak self] error in
self?.setState(.error(error))
}
)
.disposed(by: disposeBag)
}
}
// DOMAIN LAYER - UseCase
protocol PaymentUseCase {
func execute(amount: String) -> Single<PaymentResult>
}
class PaymentUseCaseImpl: PaymentUseCase {
private let paymentRepository: PaymentRepository
private let validationService: ValidationService
init(paymentRepository: PaymentRepository,
validationService: ValidationService) {
self.paymentRepository = paymentRepository
self.validationService = validationService
}
func execute(amount: String) -> Single<PaymentResult> {
// Business logic in UseCase
return validationService.validateAmount(amount)
.flatMap { validAmount in
return self.paymentRepository.processPayment(amount: validAmount)
}
}
}
// DATA LAYER - Repository
protocol PaymentRepository {
func processPayment(amount: Double) -> Single<PaymentResult>
}
class PaymentRepositoryImpl: PaymentRepository {
private let apiService: PaymentApiService
private let localStorage: PaymentLocalStorage
func processPayment(amount: Double) -> Single<PaymentResult> {
return apiService.processPayment(amount: amount)
.do(onSuccess: { [weak self] result in
self?.localStorage.savePaymentRecord(result)
})
}
}
```
### ❌ Layer Violations
```swift
// BAD: ViewModel bypassing UseCase
class PaymentViewModel: BaseViewModel<PaymentState> {
private let apiService: PaymentApiService // Wrong layer!
func processPayment() {
// Direct API call, no business logic
apiService.processPayment(amount: amount)
.subscribe(onNext: { result in
// Direct storage access
RealmManager.shared.save(result)
})
.disposed(by: disposeBag)
}
}
// BAD: Business logic in ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
func processPayment(amount: Double) {
// Validation logic in ViewModel
guard amount > 1000 else { return }
guard amount < 50_000_000 else { return }
// Business rules in ViewModel
let fee = amount * 0.01
let total = amount + fee
// This should all be in UseCase!
}
}
// BAD: Direct instantiation
class PaymentViewController: UIViewController {
// Hard-coded dependencies
private let viewModel = PaymentViewModel(
paymentUC: PaymentUseCaseImpl() // Direct instantiation
)
}
```
---
## 4. Security Examples
### ✅ Secure Payment Handling
```swift
class PaymentSecurityManager {
private let keychain = KeychainWrapper.standard
// Store in Keychain
func storePaymentToken(_ token: String, for merchantId: String) {
let key = "payment_token_\(merchantId)"
keychain.set(token, forKey: key,
withAccessibility: .whenUnlockedThisDeviceOnly)
}
func retrievePaymentToken(for merchantId: String) -> String? {
let key = "payment_token_\(merchantId)"
return keychain.string(forKey: key)
}
}
// Mask sensitive data in logs
class PaymentRequest {
let amount: Double
let merchantId: String
override var description: String {
return "PaymentRequest(amount: \(amount), merchantId: ***)"
}
}
// HTTPS with certificate pinning
class PaymentNetworkManager {
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
return URLSession(
configuration: config,
delegate: CertificatePinningDelegate(),
delegateQueue: nil
)
}()
}
```
### ❌ Security Violations
```swift
// BAD: Insecure storage
class PaymentManager {
func storePaymentToken(_ token: String) {
// UserDefaults is not secure!
UserDefaults.standard.set(token, forKey: "payment_token")
}
func processPayment(_ request: PaymentRequest) {
// Logging sensitive data!
print("Processing payment: \(request)")
print("Card number: \(request.cardNumber)")
}
}
// BAD: No certificate pinning
class PaymentNetworkManager {
func processPayment(_ request: PaymentRequest) {
let url = URL(string: "http://api.payoo.vn/payment")! // HTTP!
// No encryption
let data = try! JSONEncoder().encode(request)
// No certificate pinning
URLSession.shared.dataTask(with: url) { _, _, _ in }
}
}
```
---
## 5. UI/UX Examples
### ✅ Proper Navigation Setup
```swift
// Simple title
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Payment" // Use title property
}
}
// Title with subtitle (only when subtitle exists)
class StoreSelectionViewController: UIViewController {
private let titleDescription: String?
override func viewDidLoad() {
super.viewDidLoad()
if let description = titleDescription, !description.isEmpty {
// Only use titleView when subtitle exists
navigationItem.titleView = createTitleView(
title: "Store Selection",
description: description
)
} else {
// Use simple title when no subtitle
title = "Store Selection"
}
}
}
// Loading states with feedback
class QRSaleViewController: UIViewController {
private func bindLoadingStates() {
viewModel.isProcessing
.bind(to: loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.isProcessing
.map { !$0 }
.bind(to: processButton.rx.isEnabled)
.disposed(by: disposeBag)
viewModel.isProcessing
.map { $0 ? "Processing..." : "Process Payment" }
.bind(to: processButton.rx.title(for: .normal))
.disposed(by: disposeBag)
}
}
// Accessibility
class PaymentAmountView: UIView {
private func setupAccessibility() {
amountTextField.isAccessibilityElement = true
amountTextField.accessibilityLabel = "Payment amount"
amountTextField.accessibilityHint = "Enter the payment amount in VND"
// Dynamic Type support
amountTextField.font = UIFont.preferredFont(forTextStyle: .title2)
amountTextField.adjustsFontForContentSizeCategory = true
}
}
```
### ❌ UI/UX Issues
```swift
// BAD: Using titleView for simple title
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Unnecessary custom title view
let titleLabel = UILabel()
titleLabel.text = "Payment"
navigationItem.titleView = titleLabel // Should use title property!
}
}
// BAD: No loading feedback
class QRSaleViewController: UIViewController {
private func processPayment() {
// No loading indicator
viewModel.processPayment() // User has no feedback!
}
}
// BAD: No accessibility
class PaymentAmountView: UIView {
// No accessibility setup
// No Dynamic Type support
// Missing accessibility labels
}
```
---
## 6. Performance Examples
### ✅ Proper Memory Management
```swift
class ImageDownloadManager {
private let cache = NSCache<NSString, UIImage>()
private var activeDownloads: [String: Disposable] = [:]
func downloadImage(from url: String) -> Observable<UIImage> {
let cacheKey = url as NSString
// Check cache first
if let cachedImage = cache.object(forKey: cacheKey) {
return .just(cachedImage)
}
// Cancel existing download
activeDownloads[url]?.dispose()
let download = URLSession.shared.rx
.data(request: URLRequest(url: URL(string: url)!))
.compactMap { UIImage(data: $0) }
.do(
onNext: { [weak self] image in
self?.cache.setObject(image, forKey: cacheKey)
},
onDispose: { [weak self] in
self?.activeDownloads.removeValue(forKey: url)
}
)
.share(replay: 1, scope: .whileConnected)
activeDownloads[url] = download.connect()
return download
}
}
// Database on background thread
class TransactionRepository {
func getTransactions() -> Observable<[Transaction]> {
return Observable.collection(from: realm.objects(TransactionObject.self))
.map { results in
return results.map { Transaction(from: $0) }
}
.subscribeOn(ConcurrentScheduler.background) // Background
.observeOn(MainScheduler.instance) // Main for UI
}
}
```
### ❌ Performance Issues
```swift
// BAD: Memory leak from strong references
class ImageDownloadManager {
private var downloads: [URLSessionDataTask] = [] // Strong references
func downloadImage(from url: String) -> Observable<UIImage> {
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, _ in
// Process
}
self.downloads.append(task) // Never removed - leak!
task.resume()
return Disposables.create {
task.cancel()
// Still in downloads array!
}
}
}
}
// BAD: Blocking main thread
class TransactionRepository {
func getTransactions() -> Observable<[Transaction]> {
return Observable.create { observer in
// Blocking operation on main thread!
let realm = try! Realm()
let results = realm.objects(TransactionObject.self)
let transactions = results.map { Transaction(from: $0) }
observer.onNext(Array(transactions))
observer.onCompleted()
return Disposables.create()
}
}
}
```
---
## Common Review Scenarios
### Scenario 1: New Feature Review
**Code**: New payment processing feature
**Check**:
1. Naming: All classes/variables descriptive?
2. RxSwift: Disposal and memory management?
3. Architecture: Proper layer separation?
4. Security: Payment data handled securely?
5. Tests: Unit tests included?
6. Performance: No blocking operations?
### Scenario 2: Bug Fix Review
**Code**: Fix for crash in transaction list
**Check**:
1. Root cause addressed?
2. No force unwrapping?
3. Proper error handling added?
4. Tests for the bug scenario?
5. No new issues introduced?
### Scenario 3: Refactoring Review
**Code**: Refactor ViewModel to use Clean Architecture
**Check**:
1. UseCase layer added?
2. Business logic moved from ViewModel?
3. DI setup correctly?
4. Tests still pass?
5. No breaking changes?
---
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.
## Quick Reference Checklist
Copy this for quick reviews:
```markdown
## Review Checklist
### Naming ✅
- [ ] Classes: PascalCase, descriptive
- [ ] Variables: camelCase, meaningful
- [ ] Booleans: is/has/should/can prefix
- [ ] No abbreviations
- [ ] IBOutlets: type suffix
### RxSwift 🔄
- [ ] All subscriptions disposed
- [ ] [weak self] in closures
- [ ] Correct schedulers
- [ ] Error handling present
- [ ] Using BehaviorRelay
### Architecture 🏗️
- [ ] ViewModel → UseCase → Repository
- [ ] No business logic in ViewModel
- [ ] Dependencies injected
- [ ] BaseViewModel extended
- [ ] Repository pattern used
### Security 🔒
- [ ] Payment data in Keychain
- [ ] No sensitive logs
- [ ] HTTPS with pinning
- [ ] Input validation
### UI/UX 🎨
- [ ] title for simple titles
- [ ] titleView only with subtitle
- [ ] Accessibility configured
- [ ] Loading states shown
### Performance ⚡
- [ ] DB on background thread
- [ ] No retain cycles
- [ ] Image caching
- [ ] Memory management proper
```

View File

@@ -0,0 +1,185 @@
---
name: ios-naming-conventions
description: Check Swift naming conventions for iOS code. Validates class names, variables, functions, and IBOutlets against project standards. Use when reviewing code readability, checking abbreviations, or enforcing naming consistency in Swift files.
allowed-tools: Read, Grep, Glob
---
# iOS Naming Conventions Checker
Validate Swift code naming against Payoo Merchant project standards for clarity and consistency.
## When to Activate
- "check naming", "naming conventions", "code readability"
- "abbreviations", "variable names", "rename"
- Reviewing code quality or consistency
- Onboarding new developers
## Naming Rules Summary
### Types (Classes, Structs, Enums, Protocols)
- **PascalCase**: `PaymentViewModel`, `TransactionRepository`
- **Descriptive**: Purpose immediately clear
- **Proper suffixes**: ViewModel, ViewController, UseCase, Repository
### Variables & Properties
- **camelCase**: `paymentAmount`, `isProcessing`
- **Meaningful**: No abbreviations (except URL, ID, VC, UC)
- **Booleans**: Prefix `is`, `has`, `should`, `can`
- **Collections**: Plural names (`transactions`, `stores`)
### Functions & Methods
- **camelCase** with verb prefix
- **Actions**: `loadTransactions()`, `processPayment()`
- **Queries**: `getTransaction()`, `hasPermission()`
### IBOutlets
- **Type suffix**: `amountTextField`, `confirmButton`, `tableView`
## Review Process
### Step 1: Scan Code
Read files and identify all declarations:
- Class/struct/enum/protocol declarations
- Variable and property declarations
- Function declarations
- IBOutlet declarations
### Step 2: Check Against Rules
For each identifier, verify:
**Classes/Types**:
- ✅ PascalCase
- ✅ Descriptive (not generic like "Manager")
- ✅ No abbreviations (except standard ones)
- ✅ Proper suffix (ViewModel, UseCase, etc.)
**Variables**:
- ✅ camelCase
- ✅ Meaningful names
- ✅ Boolean prefixes (is/has/should/can)
- ✅ Plural for collections
- ✅ No single letters (except loop indices)
**Functions**:
- ✅ Verb-based names
- ✅ Clear action or query intent
- ✅ No generic names (doSomething, handle)
**IBOutlets**:
- ✅ Type suffix included
### Step 3: Generate Report
```markdown
# Naming Conventions Review
## Summary
- 🔴 Critical (meaningless): X
- 🟠 High (abbreviations): X
- 🟡 Medium (missing prefixes): X
- 🟢 Low (style): X
## Issues by Type
### Classes/Structs/Enums
**File**: `path/to/file.swift:line`
Current: `PayVC`
Should be: `PaymentViewController`
Reason: [Explanation]
### Variables/Properties
[List with specific fixes]
### Functions
[List with specific fixes]
### IBOutlets
[List with specific fixes]
## Batch Rename Suggestions
Found `amt` in 5 locations → Rename all to `paymentAmount`
## Good Examples Found ✅
[Acknowledge well-named elements]
```
## Common Violations
### ❌ Abbreviations
```swift
class PayVC { } PaymentViewController
let amt: Double paymentAmount
func procPmt() { } processPayment()
```
### ❌ Single Letters
```swift
let x = transaction currentTransaction
let a = amount paymentAmount
```
### ❌ Generic/Meaningless
```swift
class Manager { } PaymentManager
func doSomething() { } processRefundRequest()
func handle() { } handlePaymentError()
```
### ❌ Missing Prefixes
```swift
let loading: Bool isLoading: Bool
let valid: Bool isValid: Bool
```
### ❌ Missing Type Suffix
```swift
@IBOutlet weak var amount: UITextField! amountTextField
@IBOutlet weak var btn: UIButton! confirmButton
```
## Search Patterns
Use Grep to find:
- **Abbreviations**: `(let|var)\s+[a-z]{1,3}\s*[=:]`
- **IBOutlets**: `@IBOutlet.*weak var`
- **Booleans**: `(let|var)\s+[a-z]+.*:\s*Bool`
## Output Guidelines
**For each violation**:
1. File path and line number
2. Current name
3. Recommended name
4. Reason for change
5. Impact on code clarity
**Prioritize**:
- Critical: Meaningless names (hurts maintainability)
- High: Abbreviations (reduces clarity)
- Medium: Missing prefixes/suffixes
- Low: Style inconsistencies
## Quick Fixes
1. **Expand Abbreviation**: Use Xcode refactor tool
2. **Add Boolean Prefix**: Rename with is/has/should/can
3. **Add Type Suffix**: Update IBOutlet names
## Common Abbreviations to Fix
| ❌ Bad | ✅ Good |
|--------|---------|
| amt | paymentAmount |
| trx, tx | transaction |
| btn | button |
| lbl | label |
| vc | viewController |
| uc | useCase |
| repo | repository |
## Reference
**Detailed Examples**: See `examples.md` for extensive naming patterns and scenarios.

View File

@@ -0,0 +1,422 @@
# iOS Naming Conventions Examples
Comprehensive examples of good and bad naming patterns in Swift.
## Classes and Types
### ✅ Good Examples
```swift
// View Controllers
class PaymentViewController: UIViewController { }
class TransactionListViewController: UIViewController { }
class RefundConfirmationViewController: UIViewController { }
// ViewModels
class PaymentViewModel: BaseViewModel<PaymentState> { }
class StoresViewModel: BaseViewModel<StoresState> { }
class TransactionHistoryViewModel: BaseViewModel<TransactionHistoryState> { }
// Use Cases
protocol PaymentUseCase { }
class PaymentUseCaseImpl: PaymentUseCase { }
protocol RefundRequestUseCase { }
// Repositories
protocol PaymentRepository { }
class PaymentRepositoryImpl: PaymentRepository { }
// Models
struct Transaction { }
struct PaymentRequest { }
struct RefundRequest { }
// Enums
enum PaymentMethod { }
enum TransactionStatus { }
enum RefundRequestError: Error { }
```
### ❌ Bad Examples
```swift
// Too abbreviated
class PayVC: UIViewController { } // PaymentViewController
class RefReqVM { } // RefundRequestViewModel
class TrxRepo { } // TransactionRepository
// Too generic
class Manager { } // PaymentManager or specific purpose
class Helper { } // ValidationHelper or specific purpose
class Util { } // DateFormatter or specific utility
// Missing suffixes
class Payment { } // PaymentViewModel or PaymentUseCase
class Transaction { } // If it's a ViewModel TransactionViewModel
```
---
## Variables and Properties
### ✅ Good Examples
```swift
class PaymentViewModel {
// State properties - descriptive
let paymentAmount = BehaviorRelay<String>(value: "")
let selectedPaymentMethod = BehaviorRelay<PaymentMethod?>(value: nil)
let transactionResult = BehaviorRelay<TransactionResult?>(value: nil)
// Boolean properties - with prefixes
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
let hasNetworkConnection = BehaviorRelay<Bool>(value: true)
let shouldShowError = BehaviorRelay<Bool>(value: false)
let canSubmitPayment = BehaviorRelay<Bool>(value: false)
// Collections - plural
let transactions = BehaviorRelay<[Transaction]>(value: [])
let errorMessages = BehaviorRelay<[String]>(value: [])
let availablePaymentMethods = BehaviorRelay<[PaymentMethod]>(value: [])
// Dependencies - full names
private let paymentUseCase: PaymentUseCase
private let validationService: ValidationService
private let disposeBag = DisposeBag()
}
```
### ❌ Bad Examples
```swift
class PaymentViewModel {
// Abbreviated
let amt = BehaviorRelay<String>(value: "") // paymentAmount
let pmtMethod = BehaviorRelay<PaymentMethod?>(value: nil) // paymentMethod
let trxResult = BehaviorRelay<TransactionResult?>(value: nil) // transactionResult
// Generic/meaningless
let flag = BehaviorRelay<Bool>(value: false) // isProcessing
let data = BehaviorRelay<[Any]>(value: []) // transactions
let temp = BehaviorRelay<String>(value: "") // What is this?
// Boolean without prefix
let loading: Bool // isLoading
let valid: Bool // isValid
let enabled: Bool // isEnabled
// Single letter
let x = 0 // transactionCount
let a = amount // paymentAmount
// Inconsistent abbreviations
let paymentUC: PaymentUseCase // paymentUseCase
}
```
---
## Functions and Methods
### ✅ Good Examples
```swift
class TransactionViewModel {
// Actions - verb-based, descriptive
func loadTransactions() { }
func refreshTransactionList() { }
func filterTransactionsByDate(from startDate: Date, to endDate: Date) { }
func processRefundRequest(for transactionId: String) { }
func validatePaymentAmount(_ amount: String) -> Bool { }
// Queries - return information
func getTransaction(by id: String) -> Transaction? { }
func calculateTotalAmount(for transactions: [Transaction]) -> Double { }
func hasUnprocessedTransactions() -> Bool { }
func isValidPaymentAmount(_ amount: String) -> Bool { }
// State handlers - clear purpose
func handlePaymentSuccess(_ result: PaymentResult) { }
func handlePaymentError(_ error: Error) { }
func handleNetworkConnectionLost() { }
// Setup/Configuration - clear intent
func setupUI() { }
func configureTableView() { }
func bindViewModel() { }
}
```
### ❌ Bad Examples
```swift
class TransactionViewModel {
// Too vague
func doSomething() { } // processRefundRequest()
func process() { } // Process what? processPayment()
func get() { } // Get what? getTransactions()
func handle() { } // Handle what? handleError()
func go() { } // Go where? navigateToDetails()
// Noun-based instead of verb-based
func transaction() { } // loadTransaction() or getTransaction()
func payment() { } // processPayment()
// Abbreviated
func procPmt() { } // processPayment()
func getTrx() { } // getTransaction()
func valAmt(_ amt: String) { } // validateAmount(_ amount: String)
}
```
---
## IBOutlets
### ✅ Good Examples
```swift
class PaymentViewController: UIViewController {
// Text fields - with TextField suffix
@IBOutlet weak var paymentAmountTextField: UITextField!
@IBOutlet weak var merchantCodeTextField: UITextField!
@IBOutlet weak var notesTextField: UITextField!
// Labels - with Label suffix
@IBOutlet weak var currencyLabel: UILabel!
@IBOutlet weak var totalAmountLabel: UILabel!
@IBOutlet weak var errorMessageLabel: UILabel!
// Buttons - with Button suffix
@IBOutlet weak var confirmPaymentButton: UIButton!
@IBOutlet weak var cancelButton: UIButton!
@IBOutlet weak var submitButton: UIButton!
// Tables and collections - with TableView/CollectionView suffix
@IBOutlet weak var transactionTableView: UITableView!
@IBOutlet weak var storesCollectionView: UICollectionView!
// Other views - with type suffix
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
@IBOutlet weak var headerView: UIView!
@IBOutlet weak var footerContainerView: UIView!
@IBOutlet weak var paymentMethodSegmentedControl: UISegmentedControl!
}
```
### ❌ Bad Examples
```swift
class PaymentViewController: UIViewController {
// Missing type suffix
@IBOutlet weak var amount: UITextField! // amountTextField
@IBOutlet weak var currency: UILabel! // currencyLabel
@IBOutlet weak var confirm: UIButton! // confirmButton
// Abbreviated
@IBOutlet weak var lbl: UILabel! // titleLabel
@IBOutlet weak var btn: UIButton! // submitButton
@IBOutlet weak var txtField: UITextField! // amountTextField
// Too generic
@IBOutlet weak var table: UITableView! // transactionTableView
@IBOutlet weak var view: UIView! // headerView
@IBOutlet weak var loading: UIActivityIndicatorView! // loadingIndicator
}
```
---
## Test Naming
### ✅ Good Examples
```swift
class PaymentViewModelTests: XCTestCase {
// Format: test[MethodName]_[Scenario]_[ExpectedResult]
func testProcessPayment_WithValidAmount_CompletesSuccessfully() { }
func testProcessPayment_WithEmptyAmount_ShowsValidationError() { }
func testProcessPayment_WithNetworkError_ShowsErrorState() { }
func testLoadTransactions_WithCachedData_ReturnsDataImmediately() { }
func testRefundRequest_WhenAmountExceedsTransaction_Fails() { }
func testValidateAmount_BelowMinimum_ReturnsFalse() { }
func testValidateAmount_AboveMaximum_ReturnsFalse() { }
func testValidateAmount_ValidRange_ReturnsTrue() { }
func testSubmitRefund_WithValidData_UpdatesStateToSuccess() { }
func testSubmitRefund_WithInvalidData_UpdatesStateToError() { }
}
```
### ❌ Bad Examples
```swift
class PaymentViewModelTests: XCTestCase {
func test1() { } // Meaningless number
func testPayment() { } // Too vague
func testError() { } // What error scenario?
func testStuff() { } // Meaningless
func test_payment_works() { } // snake_case (wrong)
}
```
---
## Complete Class Examples
### ✅ Well-Named Class
```swift
class PaymentProcessingViewModel: BaseViewModel<PaymentState> {
// Dependencies - full, descriptive names
private let paymentUseCase: PaymentUseCase
private let validationService: ValidationService
private let disposeBag = DisposeBag()
// Input properties - clear, descriptive
let paymentAmount = BehaviorRelay<String>(value: "")
let selectedMerchantId = BehaviorRelay<String?>(value: nil)
let additionalNotes = BehaviorRelay<String>(value: "")
// State properties - boolean with prefixes
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
let hasValidPaymentAmount = BehaviorRelay<Bool>(value: false)
let shouldEnableSubmitButton = BehaviorRelay<Bool>(value: false)
// Output properties - descriptive
let paymentResult = BehaviorRelay<PaymentResult?>(value: nil)
let errorMessage = BehaviorRelay<String?>(value: nil)
// Initializer - parameter names match properties
init(paymentUseCase: PaymentUseCase, validationService: ValidationService) {
self.paymentUseCase = paymentUseCase
self.validationService = validationService
super.init()
setupValidation()
}
// Methods - verb-based, descriptive
func processPaymentRequest() { }
func validatePaymentAmount() -> Bool { }
func clearPaymentForm() { }
func handlePaymentSuccess(_ result: PaymentResult) { }
func handlePaymentError(_ error: Error) { }
private func setupValidation() { }
private func updateSubmitButtonState() { }
}
```
### ❌ Poorly Named Class
```swift
class PaymentVM: BaseViewModel<PaymentState> { // Abbreviated
// Abbreviated dependencies
private let pmtUC: PaymentUseCase // paymentUseCase
private let valService: ValidationService // validationService
private let bag = DisposeBag() // disposeBag
// Abbreviated/unclear properties
let amt = BehaviorRelay<String>(value: "") // paymentAmount
let mid = BehaviorRelay<String?>(value: nil) // merchantId
let notes = BehaviorRelay<String>(value: "") // Could be clearer
// Boolean without prefix
let processing = BehaviorRelay<Bool>(value: false) // isProcessing
let valid = BehaviorRelay<Bool>(value: false) // hasValidAmount
// Generic names
let result = BehaviorRelay<PaymentResult?>(value: nil) // paymentResult
let error = BehaviorRelay<String?>(value: nil) // errorMessage
// Abbreviated initializer
init(uc: PaymentUseCase, val: ValidationService) { // Bad parameter names
self.pmtUC = uc
self.valService = val
super.init()
}
// Vague method names
func process() { } // processPaymentRequest()
func validate() -> Bool { } // validatePaymentAmount()
func clear() { } // clearPaymentForm()
func handle(_ r: PaymentResult) { } // handlePaymentSuccess()
}
```
---
## Refactoring Examples
### Example: Refactoring Poor Names
#### Before (Bad)
```swift
class TrxVM: BaseViewModel<TrxState> {
let trxs = BehaviorRelay<[Trx]>(value: [])
let loading = BehaviorRelay<Bool>(value: false)
func getTrx(id: String) -> Trx? {
return trxs.value.first { $0.id == id }
}
func proc() {
loading.accept(true)
// Process transactions
}
}
```
#### After (Good)
```swift
class TransactionViewModel: BaseViewModel<TransactionState> {
let transactions = BehaviorRelay<[Transaction]>(value: [])
let isLoading = BehaviorRelay<Bool>(value: false)
func getTransaction(by id: String) -> Transaction? {
return transactions.value.first { $0.id == id }
}
func processTransactions() {
isLoading.accept(true)
// Process transactions
}
}
```
**Changes Made**:
1. `TrxVM``TransactionViewModel` (full names, proper suffix)
2. `trxs``transactions` (no abbreviation, plural)
3. `loading``isLoading` (boolean prefix)
4. `getTrx``getTransaction` (full name, clear parameters)
5. `proc``processTransactions` (verb-based, descriptive)
---
## Quick Reference Checklist
Use this when reviewing naming:
```markdown
## Naming Conventions Checklist
### Classes/Structs/Enums
- [ ] PascalCase
- [ ] Descriptive and meaningful
- [ ] Proper suffix (ViewModel, UseCase, etc.)
- [ ] No abbreviations
### Variables/Properties
- [ ] camelCase
- [ ] Meaningful names
- [ ] Booleans have is/has/should/can prefix
- [ ] Collections are plural
- [ ] No single letters (except loops)
- [ ] No abbreviations
### Functions/Methods
- [ ] camelCase
- [ ] Verb-based (actions) or get/has/is (queries)
- [ ] Descriptive of purpose
- [ ] Clear parameter names
### IBOutlets
- [ ] Include type suffix
- [ ] Descriptive of purpose
- [ ] camelCase
### General
- [ ] No generic names (Manager, Helper, Util)
- [ ] Consistent naming style
- [ ] Easy to understand without context
```

View File

@@ -0,0 +1,605 @@
---
name: mcp-tool-generator
description: Generate new MCP tools for GitLab operations following the project's standardized pattern. Creates complete TypeScript files with imports, registration functions, Zod schemas, error handling, and format options. Supports simple CRUD operations, complex multi-action tools, and advanced patterns like discussion management. Use when "create mcp tool", "generate gitlab tool", "new tool for", "add tool to gitlab", or building new GitLab integration features.
tools: [Read, Write, Glob, Grep]
---
# MCP Tool Generator
Generate new MCP tools following the standardized patterns from the project. Creates complete tool files with proper imports, Zod schemas, error handling, and GitLab API integration.
## Activation Triggers
- "create an mcp tool for..."
- "generate a gitlab tool to..."
- "I need a new tool that..."
- "add a tool for [operation]"
- "create tool to [action] [resource]"
## Tool Types Supported
### 1. Simple CRUD Tools
Basic get/list/create/update/delete operations with standard patterns:
- Get single resource (issue, MR, milestone, etc.)
- List multiple resources with filtering and pagination
- Create new resources
- Update existing resources
- Delete resources
**Pattern**: `gitlab-[action]-[resource]` (e.g., `gitlab-get-issue`, `gitlab-list-pipelines`)
### 2. Multi-Action Tools
Comprehensive tools that handle multiple related operations in one tool:
- Multiple actions via `action` enum parameter
- Conditional logic based on action type
- Structured responses with status/action/message format
- More efficient than multiple separate tools
**Pattern**: `gitlab-[resource]-[operation]` (e.g., `gitlab-manage-issue`)
### 3. Complex Operation Tools
Tools with advanced logic:
- Discussion/comment management with update detection
- Multi-step workflows
- Direct API calls using fetch for specific needs
- Position-based operations (code reviews, inline comments)
**Pattern**: Based on specific operation (e.g., `gitlab-review-merge-request-code`)
## Autonomous Generation Process
### Step 1: Analyze User Request
Extract key information:
1. **Tool Type**: Simple CRUD, multi-action, or complex?
2. **Tool Purpose**: What GitLab operation? (e.g., "get merge request details", "manage issues", "review code")
3. **Resource Type**: What GitLab entity? (issue, MR, branch, milestone, pipeline, label, etc.)
4. **Action Type**: What operation? (get, list, create, update, delete, search, manage, review, etc.)
5. **Required Parameters**: What inputs needed? (projectname, IID, branch name, action, etc.)
6. **Optional Parameters**: What's optional? (format, labels, assignee, filters, etc.)
7. **Special Features**: Multi-action? Position-based? Discussion management?
### Step 2: Auto-Generate Names
**Tool Name** (kebab-case):
- Simple CRUD: `gitlab-[action]-[resource]`
- Examples: `gitlab-get-merge-request`, `gitlab-list-pipelines`, `gitlab-create-branch`
- Multi-action: `gitlab-[manage|handle]-[resource]`
- Examples: `gitlab-manage-issue`, `gitlab-handle-milestone`
- Complex: `gitlab-[specific-operation]`
- Examples: `gitlab-review-merge-request-code`, `gitlab-find-related-issues`
**Function Name** (PascalCase):
- Pattern: `register[Action][Resource]`
- Examples:
- `gitlab-get-merge-request``registerGetMergeRequest`
- `gitlab-manage-issue``registerManageIssue`
- `gitlab-review-merge-request-code``registerReviewMergeRequestCode`
**File Name** (kebab-case):
- Pattern: `gitlab-[tool-name].ts`
- Location: `src/tools/gitlab/`
### Step 3: Select Tool Pattern
#### Pattern A: Simple CRUD Tool
**Use when**: Single operation (get, list, create, update, delete)
**Standard features**:
- `projectname` parameter (optional, with prompt fallback)
- `format` parameter (detailed/concise) for get/list operations
- HTML content cleaning with `cleanGitLabHtmlContent()`
- Project validation before API calls
- Descriptive error messages
- Emojis in concise format
**Template structure**:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function register{FunctionName}(server: McpServer) {
server.registerTool(
"{tool-name}",
{
title: "{Human Readable Title}",
description: "{Detailed description}",
inputSchema: {
{param1}: z.{type}().describe("{description}"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ {params}, projectname, format = "detailed" }) => {
try {
// Standard workflow
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: String(e) }) }] };
}
}
);
}
```
#### Pattern B: Multi-Action Tool
**Use when**: Multiple related operations on same resource type
**Standard features**:
- `action` parameter with enum of actions
- Switch/case logic for each action
- Structured responses: `{ status: "success"/"failure", action: "...", message: "...", [resource]: {...} }`
- Direct API calls using fetch when needed
- Conditional parameters based on action
- No format parameter (uses structured JSON)
**Template structure**:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import fetch from 'node-fetch';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function register{FunctionName}(server: McpServer) {
server.registerTool(
"{tool-name}",
{
title: "{Human Readable Title}",
description: "{Comprehensive description covering all actions}",
inputSchema: {
{resourceId}: z.number().describe("The ID/IID of the resource"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform"),
// Conditional parameters for different actions
param1: z.{type}().optional().describe("For action1: description"),
param2: z.{type}().optional().describe("For action2: description")
}
},
async ({ {resourceId}, projectname, action, {params} }) => {
try {
// Get project and resource
const projectName = projectname || await getProjectNameFromUser(server, false, "prompt");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found" }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Project "${projectName}" not found` }) }] };
}
// Get resource first
const rawResource = await service.get{Resource}(projectId, {resourceId});
if (!rawResource) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Resource not found` }) }] };
}
const resource = cleanGitLabHtmlContent(rawResource, ['description', 'title']);
// Handle actions
switch (action) {
case "action1":
// Implementation
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'action1',
message: 'Action completed',
{resource}: { /* key fields */ }
}, null, 2) }] };
case "action2":
// Implementation
break;
default:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Unknown action "${action}"`
}, null, 2) }] };
}
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: String(e)
}, null, 2) }] };
}
}
);
}
```
#### Pattern C: Complex Operation Tool
**Use when**: Advanced logic like discussion management, position-based operations, multi-step workflows
**Standard features**:
- Specialized parameters (may not include projectname if using projectId directly)
- Custom logic for specific use cases
- May use direct API calls
- May fetch and update existing data
- Structured responses appropriate to operation
**Template structure**: Highly variable based on specific needs
### Step 4: Generate Zod Schema
**Common Parameter Patterns**:
```typescript
// IDs (internal issue/MR number)
{name}Iid: z.number().describe("The internal ID (IID) of the {resource} to {action}")
// Project ID (for tools that need direct ID)
projectId: z.number().describe("The project ID")
// Names/identifiers
{name}: z.string().describe("{Resource} name (e.g., 'feature/user-auth')")
// Action enums (for multi-action tools)
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform on the {resource}")
// Optional filters
state: z.enum(["opened", "closed", "all"]).optional().describe("Filter by state (default: 'opened')")
labels: z.string().optional().describe("Comma-separated list of label names to filter by")
// OR for multi-action tools:
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add")
// Pagination
page: z.number().optional().describe("Page number for pagination (default: 1)")
perPage: z.number().optional().describe("Number of items per page (default: 20, max: 100)")
// Dates
dueDate: z.string().optional().describe("Due date in ISO 8601 format (YYYY-MM-DD)")
// Position-based parameters (for code review tools)
baseSha: z.string().describe("Base SHA for the diff")
startSha: z.string().describe("Start SHA for the diff")
headSha: z.string().describe("Head SHA for the diff")
newPath: z.string().describe("Path to the file being reviewed")
newLine: z.number().optional().describe("Line number in the new file")
// Project (standard for simple CRUD, optional for complex tools)
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)")
// Format (only for simple get/list operations)
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
```
**Important notes**:
- Add `.describe()` with clear examples for all parameters
- Use `z.array(z.string())` for arrays in multi-action tools
- Note when square brackets `[]` are allowed in descriptions for paths/labels/markdown
- Make parameters optional when sensible defaults exist
### Step 5: Generate Response Formats
#### Simple CRUD Tools (Pattern A)
**Concise format** (with emojis):
```typescript
if (format === "concise") {
return { content: [{ type: "text", text:
`{emoji} {Resource} #{id}: {title}\n` +
`📊 Status: {state}\n` +
`👤 {role}: {user}\n` +
`🏷️ Labels: {labels}\n` +
`🎯 Milestone: {milestone}\n` +
`📅 Due: {due_date}\n` +
`🔗 URL: {web_url}`
}] };
}
```
**Detailed format** (full JSON):
```typescript
return { content: [{ type: "text", text: JSON.stringify({resource}, null, 2) }] };
```
**Emoji Guide**:
- 🔍 - Get/View operations
- 📋 - List operations
- ✨ - Create operations
- 🔄 - Update operations
- 🗑️ - Delete operations
- 📊 - Status/State
- 👤 - User/Assignee
- 🏷️ - Labels
- 🎯 - Milestone
- 📅 - Dates
- 🔗 - URLs
- ✅ - Success/Completed
- ❌ - Error/Failed
#### Multi-Action Tools (Pattern B)
**Structured JSON format**:
```typescript
// Success response
{
status: 'success',
action: 'action-name',
message: 'Human-readable success message',
{resource}: {
id: resource.id,
iid: resource.iid,
title: resource.title,
webUrl: resource.web_url,
// Other key fields relevant to the action
}
}
// Failure response
{
status: 'failure',
action: 'action-name',
error: 'Detailed error message with context',
{resource}: {
id: resource.id,
iid: resource.iid,
title: resource.title,
webUrl: resource.web_url
}
}
```
#### Complex Tools (Pattern C)
Custom format based on operation needs. Examples:
```typescript
// Discussion update/create
{
action: "updated" | "created",
discussion_id: "...",
note_id: "...",
updated_note: {...}
}
```
### Step 6: Add Error Handling
**Standard Error Patterns**:
```typescript
// Project not selected (for tools with projectname parameter)
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: "Project not found or not selected. Please provide a valid project name."
}) }] };
}
// Project not found
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.`
}) }] };
}
// Resource not found
if (!resource) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `{Resource} not found. Please verify the {parameters} are correct.`
}) }] };
}
// Missing required parameters (for multi-action tools)
if (!requiredParam) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: "Required parameter missing. Please specify...",
{resource}: { /* minimal info */ }
}, null, 2) }] };
}
// API call failure (for multi-action tools using fetch)
if (!response.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: `Failed to {action}. Status: ${response.status}`,
{resource}: { /* minimal info */ }
}, null, 2) }] };
}
// General error (catch block)
catch (e) {
// For simple CRUD tools:
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Error {operation}: ${String(e)}. Please check your GitLab connection and permissions.`
}) }] };
// For multi-action tools:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error {operation}: ${String(e)}`
}, null, 2) }] };
}
```
### Step 7: Register in gitlab-tool.ts
After creating the tool file, add registration:
```typescript
// In src/tools/gitlab-tool.ts
// Add import at top
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
// Add registration in registerGitlabTools function
export function registerGitlabTools(server: McpServer) {
// ... other registrations
register{FunctionName}(server);
}
```
## Interactive Generation Workflow
### Ask User (Only if unclear):
1. **Tool Type**:
- "Is this a simple CRUD operation, multi-action tool, or complex operation?"
- Clarify if multiple actions should be combined in one tool
2. **Tool Purpose**:
- "What GitLab operation should this tool perform?"
- Examples: "Get merge request details", "Manage issues (get, update, close)", "Review code inline"
3. **Required Parameters**:
- "What parameters are required?"
- Examples: "merge request IID", "issue IID and action type", "project ID and position data"
4. **Optional Parameters**:
- "Any optional filters or options?"
- Examples: "state filter", "label filter", "format option"
5. **API Method** (if not obvious):
- "Which GitLab service method to use?"
- Check `src/services/gitlab-client.ts` for available methods
- Note if direct fetch API calls are needed
### Generate Files:
1. **Create tool file**: `src/tools/gitlab/gitlab-{tool-name}.ts`
2. **Show registration code** for `src/tools/gitlab-tool.ts`
3. **Provide usage examples** based on tool type
## Output Format
After generating the tool:
```markdown
✅ MCP Tool Created: {tool-name}
📁 Files Created:
- `src/tools/gitlab/gitlab-{tool-name}.ts`
🔧 Type: {Simple CRUD | Multi-Action | Complex Operation}
🔧 Function: register{FunctionName}
📝 Next Steps:
1. Add registration to `src/tools/gitlab-tool.ts`:
```typescript
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
// In registerGitlabTools:
register{FunctionName}(server);
```
2. Rebuild the project:
```bash
npm run build
```
3. Test the tool:
```bash
npm run dev
```
🎯 Usage Examples:
{Type-specific examples}
📖 Tool registered as: "{tool-name}"
```
## GitLab Service Methods Reference
Common methods available in `gitlab-client.ts`. Latest update 31/10/2025:
**Issues**:
- `getIssue(projectId, iid)`
- `getIssues(projectId, options)`
- `createIssue(projectId, data)`
- `updateIssue(projectId, iid, data)`
**Merge Requests**:
- `getMergeRequest(projectId, iid)`
- `getMergeRequests(projectId, options)`
- `createMergeRequest(projectId, data)`
- `updateMergeRequest(projectId, iid, data)`
- `approveMergeRequest(projectId, iid)`
- `getMrDiscussions(projectId, iid)`
- `addMrComments(projectId, iid, data)`
- `updateMrDiscussionNote(projectId, iid, discussionId, noteId, body)`
**Branches**:
- `getBranches(projectId, options)`
- `createBranch(projectId, branchName, ref)`
- `deleteBranch(projectId, branchName)`
**Pipelines**:
- `getPipelines(projectId, options)`
- `getPipeline(projectId, pipelineId)`
- `createPipeline(projectId, ref)`
**Milestones**:
- `getMilestone(projectId, milestoneId)`
- `getMilestones(projectId, options)`
- `createMilestone(projectId, data)`
- `updateMilestone(projectId, milestoneId, data)`
**Projects**:
- `getProjectId(projectName)`
- `getProject(projectId)`
- `searchProjects(search)`
**Users**:
- `getUserIdByUsername(username)`
**Direct Fetch API**:
For operations not covered by service methods, use direct fetch:
```typescript
const response = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/{endpoint}`, {
method: 'PUT' | 'POST' | 'GET' | 'DELETE',
headers: service['getHeaders'](),
body: JSON.stringify({...})
});
```
## Key Patterns to Follow
1. **Always use appropriate tool pattern** based on operation type
2. **Simple CRUD tools**: Include `projectname` and `format` parameters
3. **Multi-action tools**: Use `action` enum and structured responses
4. **Always clean HTML content** with `cleanGitLabHtmlContent()` where applicable
5. **Always validate project exists** before API calls (if using projectname)
6. **Always use descriptive error messages** with context
7. **Always use emojis in concise format** for simple CRUD tools
8. **Always follow kebab-case** for file and tool names
9. **Always follow PascalCase** for function names
10. **Always provide detailed Zod descriptions** with examples
11. **Always handle null/undefined responses** gracefully
12. **Multi-action tools**: Return structured JSON with status/action/message
13. **Direct API calls**: Use fetch and check response.ok
14. **Note square bracket support**: Add notes about `[]` support in descriptions where relevant (file paths, labels, markdown)
## Quality Checklist
Before presenting the generated tool:
- ✅ File name is kebab-case
- ✅ Function name is PascalCase with "register" prefix
- ✅ All imports are correct
- ✅ Zod schema has detailed descriptions
- ✅ Appropriate tool pattern selected (Simple CRUD / Multi-Action / Complex)
- ✅ For simple CRUD: projectname optional, format parameter included
- ✅ For multi-action: action enum, structured responses, conditional params
- ✅ HTML content cleaned where applicable
- ✅ Error messages are descriptive and actionable
- ✅ Response format matches tool type
- ✅ Try-catch wraps the entire handler
- ✅ All responses follow `{ content: [{ type: "text", text: ... }] }` format
- ✅ Tool follows MCP SDK patterns
- ✅ Code matches project conventions from CLAUDE.md
---
**Ready to generate MCP tools!** Tell me what GitLab operation you want to create a tool for.

View File

@@ -0,0 +1,556 @@
# MCP Tool Generator Examples
Complete examples of generated MCP tools following the standardized patterns. Includes simple CRUD tools, multi-action tools, and complex operation tools.
## Example 1: Simple CRUD - Get Merge Request Details
**Tool Type**: Simple CRUD (Pattern A)
### User Request
"Create a tool to get merge request details by IID"
### Generated File: `src/tools/gitlab/gitlab-get-merge-request.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerGetMergeRequest(server: McpServer) {
server.registerTool(
"gitlab-get-merge-request",
{
title: "Get Merge Request Details",
description: "Retrieve detailed information for a specific merge request by IID in a GitLab project. Returns merge request metadata including title, description, state, author, assignee, reviewers, labels, milestone, source/target branches, and approval status. Use this when you need comprehensive information about a specific merge request.",
inputSchema: {
mergeRequestIid: z.number().describe("The internal ID (IID) of the merge request to retrieve"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ mergeRequestIid, projectname, format = "detailed" }) => {
const iid = mergeRequestIid as number;
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for getting merge request");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
const rawMr = await service.getMergeRequest(projectId, iid);
if (!rawMr) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Merge request !${iid} not found in project "${projectName}". Please verify the merge request IID is correct.` }) }] };
}
// Clean HTML content from merge request fields
const mr = cleanGitLabHtmlContent(rawMr, ['description', 'title']);
// Format response based on requested format
if (format === "concise") {
const conciseInfo = {
title: mr.title,
state: mr.state,
author: mr.author?.name || "Unknown",
assignee: mr.assignee?.name || "Unassigned",
labels: mr.labels || [],
milestone: mr.milestone?.title || "No milestone",
source_branch: mr.source_branch,
target_branch: mr.target_branch,
web_url: mr.web_url
};
return { content: [{ type: "text", text: `🔍 MR !${iid}: ${mr.title}\n📊 Status: ${mr.state}\n👤 Author: ${conciseInfo.author}\n👤 Assignee: ${conciseInfo.assignee}\n🏷 Labels: ${conciseInfo.labels.join(', ') || 'None'}\n🎯 Milestone: ${conciseInfo.milestone}\n🔀 ${conciseInfo.source_branch}${conciseInfo.target_branch}\n🔗 URL: ${mr.web_url}` }] };
}
return { content: [{ type: "text", text: JSON.stringify(mr, null, 2) }] };
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error retrieving merge request !${iid}: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
}
}
);
}
```
### Registration in `gitlab-tool.ts`
```typescript
import { registerGetMergeRequest } from './gitlab/gitlab-get-merge-request';
export function registerGitlabTools(server: McpServer) {
// ... other registrations
registerGetMergeRequest(server);
}
```
---
## Example 2: Multi-Action Tool - Manage Issues
**Tool Type**: Multi-Action (Pattern B)
### User Request
"Create a tool that can manage issues - get details, close, reopen, add labels, set assignees, and set due dates"
### Generated File: `src/tools/gitlab/gitlab-manage-issue.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import fetch from 'node-fetch';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerManageIssue(server: McpServer) {
server.registerTool(
"gitlab-manage-issue",
{
title: "Manage GitLab Issue",
description: "Comprehensive issue management tool that can get, update, or modify issues in a single operation. More efficient than using multiple separate tools. Supports getting issue details, updating status, adding labels, setting assignees, and modifying due dates.",
inputSchema: {
issueIid: z.number().describe("The internal ID (IID) of the issue to manage"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
action: z.enum(["get", "close", "reopen", "add-labels", "set-assignee", "set-due-date"]).describe("Action to perform on the issue"),
// Parameters for different actions
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add to the issue. Square brackets [] are allowed in label names."),
assignee_username: z.string().optional().describe("For set-assignee action: username to assign the issue to"),
due_date: z.string().optional().describe("For set-due-date action: due date in YYYY-MM-DD format")
}
},
async ({ issueIid, projectname, action, labels, assignee_username, due_date }) => {
const iid = issueIid as number;
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for issue management");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
// Get issue first for all actions
const rawIssue = await service.getIssue(projectId, iid);
if (!rawIssue) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Issue #${iid} not found in project "${projectName}". Please verify the issue IID is correct.` }) }] };
}
// Clean HTML content from issue fields
const issue = cleanGitLabHtmlContent(rawIssue, ['description', 'title']);
switch (action) {
case "get":
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'get',
issue: {
id: issue.id,
iid: issue.iid,
title: issue.title,
webUrl: issue.web_url,
state: issue.state,
assignee: issue.assignee?.name || null,
labels: issue.labels || [],
milestone: issue.milestone?.title || null,
dueDate: issue.due_date || null,
description: issue.description
}
}, null, 2) }] };
case "close":
const closeResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
method: 'PUT',
headers: service['getHeaders'](),
body: JSON.stringify({ state_event: "close" })
});
if (!closeResponse.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'close',
error: `Failed to close issue #${iid}. Status: ${closeResponse.status}`,
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const closedIssue = await closeResponse.json();
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'close',
message: `Issue #${iid} has been closed successfully`,
issue: {
id: closedIssue.id,
iid: closedIssue.iid,
title: closedIssue.title,
webUrl: closedIssue.web_url,
state: closedIssue.state
}
}, null, 2) }] };
case "add-labels":
if (!labels || labels.length === 0) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'add-labels',
error: "No labels provided. Please specify labels to add using the 'labels' parameter.",
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const currentLabels = issue.labels || [];
const newLabels = [...new Set([...currentLabels, ...labels])];
const labelsResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
method: 'PUT',
headers: service['getHeaders'](),
body: JSON.stringify({ labels: newLabels.join(',') })
});
if (!labelsResponse.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'add-labels',
error: `Failed to add labels. Status: ${labelsResponse.status}`,
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const labeledIssue = await labelsResponse.json();
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'add-labels',
message: `Added labels to issue #${iid}`,
addedLabels: labels,
issue: {
id: labeledIssue.id,
iid: labeledIssue.iid,
title: labeledIssue.title,
webUrl: labeledIssue.web_url,
labels: labeledIssue.labels
}
}, null, 2) }] };
default:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: `Unknown action "${action}"`
}, null, 2) }] };
}
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error managing issue #${iid}: ${String(e)}`
}, null, 2) }] };
}
}
);
}
```
---
## Example 3: Complex Operation - Review Merge Request Code
**Tool Type**: Complex Operation (Pattern C)
### User Request
"Create a tool to add inline code review comments on merge requests with position tracking and duplicate detection"
### Generated File: `src/tools/gitlab/gitlab-review-merge-request-code.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { getGitLabService } from './gitlab-shared';
export function registerReviewMergeRequestCode(server: McpServer) {
server.registerTool(
"gitlab-review-merge-request-code",
{
title: "Review Merge Request Code",
description: "Add or update a code review comment on a merge request at a specific file and line position. This tool is designed for inline code reviews - it intelligently updates existing comments at the same position instead of creating duplicates. Requires diff SHA references (base, start, head) and file path with optional line numbers.",
inputSchema: {
projectId: z.number().describe("The project ID"),
mrIid: z.number().describe("The merge request IID"),
body: z.string().describe("The review comment body. Square brackets [] are allowed and commonly used in code references, markdown links, and examples."),
positionType: z.string().default("text").describe("Position type (text, image, etc.)"),
baseSha: z.string().describe("Base SHA for the diff"),
startSha: z.string().describe("Start SHA for the diff"),
headSha: z.string().describe("Head SHA for the diff"),
newPath: z.string().describe("Path to the file being reviewed. Square brackets [] are allowed in file paths."),
newLine: z.number().optional().describe("Line number in the new file (for line comments)"),
oldPath: z.string().optional().describe("Path to the old file (defaults to newPath). Square brackets [] are allowed in file paths."),
oldLine: z.number().optional().describe("Line number in the old file (for line comments)")
}
},
async ({ projectId, mrIid, body, positionType, baseSha, startSha, headSha, newPath, newLine, oldPath, oldLine }) => {
const pid = projectId as number;
const iid = mrIid as number;
const commentBody = body as string;
const posType = positionType as string;
const base = baseSha as string;
const start = startSha as string;
const head = headSha as string;
const path = newPath as string;
const line = newLine as number | undefined;
const oldFilePath = (oldPath as string | undefined) || path;
const oldFileLine = oldLine as number | undefined;
try {
const service = await getGitLabService(server);
// Get existing discussions to check for existing review comments
const discussions = await service.getMrDiscussions(String(pid), iid);
// Find existing review comment at the same position
let existingDiscussion = null;
let existingNote = null;
for (const discussion of discussions) {
if (discussion.notes && discussion.notes.length > 0) {
const firstNote = discussion.notes[0];
// Check if the position matches our target position
if (firstNote.position &&
firstNote.position.new_path === path &&
firstNote.position.base_sha === base &&
firstNote.position.head_sha === head &&
firstNote.position.start_sha === start) {
// Check if line position matches (if specified)
const positionMatches = line !== undefined ?
firstNote.position.new_line === line :
!firstNote.position.new_line;
if (positionMatches) {
existingDiscussion = discussion;
existingNote = firstNote;
break;
}
}
}
}
let result;
if (existingNote && existingDiscussion) {
// Update existing comment
result = await service.updateMrDiscussionNote(
String(pid),
iid,
existingDiscussion.id,
existingNote.id,
commentBody
);
return {
content: [{
type: "text",
text: JSON.stringify({
action: "updated",
discussion_id: existingDiscussion.id,
note_id: existingNote.id,
updated_note: result
})
}]
};
} else {
// Create new comment
const position: any = {
position_type: posType,
base_sha: base,
start_sha: start,
head_sha: head,
new_path: path,
old_path: oldFilePath
};
if (line !== undefined) {
position.new_line = line;
}
if (oldFileLine !== undefined) {
position.old_line = oldFileLine;
}
const data = { body: commentBody, position };
result = await service.addMrComments(String(pid), iid, data);
return {
content: [{
type: "text",
text: JSON.stringify({
action: "created",
discussion: result
})
}]
};
}
} catch (e) {
return {
content: [{
type: "text",
text: JSON.stringify({ type: "error", error: String(e) })
}]
};
}
}
);
}
```
---
## Example 4: Simple CRUD - List Pipelines
**Tool Type**: Simple CRUD (Pattern A)
### User Request
"I need a tool to list all pipelines with status filtering and pagination"
### Generated File: `src/tools/gitlab/gitlab-list-pipelines.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerListPipelines(server: McpServer) {
server.registerTool(
"gitlab-list-pipelines",
{
title: "List Pipelines",
description: "Retrieve a list of pipelines for a GitLab project. Supports filtering by ref (branch/tag), status, and pagination. Returns pipeline information including ID, status, ref, commit details, and timestamps. Use this to monitor CI/CD pipeline execution, check build status, or find specific pipeline runs.",
inputSchema: {
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
ref: z.string().optional().describe("Filter pipelines by git reference (branch or tag name, e.g., 'main', 'develop')"),
status: z.enum(["running", "pending", "success", "failed", "canceled", "skipped", "manual"]).optional().describe("Filter pipelines by status"),
page: z.number().optional().describe("Page number for pagination (default: 1)"),
perPage: z.number().optional().describe("Number of pipelines per page (default: 20, max: 100)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ projectname, ref, status, page = 1, perPage = 20, format = "detailed" }) => {
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for listing pipelines");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
const options: any = { page, per_page: perPage };
if (ref) options.ref = ref;
if (status) options.status = status;
const rawPipelines = await service.getPipelines(projectId, options);
if (!rawPipelines || rawPipelines.length === 0) {
return { content: [{ type: "text", text: JSON.stringify({ type: "info", message: `No pipelines found in project "${projectName}" with the specified filters.` }) }] };
}
const pipelines = rawPipelines.map(p => cleanGitLabHtmlContent(p, []));
if (format === "concise") {
const summary = pipelines.map(p =>
`📋 Pipeline #${p.id} | ${p.status} | ${p.ref} | ${new Date(p.created_at).toLocaleDateString()}`
).join('\n');
return { content: [{ type: "text", text: `📋 Found ${pipelines.length} pipeline(s) in "${projectName}":\n\n${summary}` }] };
}
return { content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }] };
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error listing pipelines: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
}
}
);
}
```
---
## Common Patterns Summary
### Tool Pattern Selection Guide
| Tool Type | When to Use | Key Features | Example |
|-----------|-------------|--------------|---------|
| **Simple CRUD** | Single operation on resource | projectname, format, emojis | `gitlab-get-issue` |
| **Multi-Action** | Multiple operations on same resource | action enum, structured responses | `gitlab-manage-issue` |
| **Complex** | Advanced logic, discussions, position-based | Custom parameters, specialized logic | `gitlab-review-merge-request-code` |
### Response Format Patterns
**Simple CRUD - Concise**:
```typescript
if (format === "concise") {
return { content: [{ type: "text", text:
`🔍 Resource #${id}: ${title}\n` +
`📊 Status: ${state}\n` +
`🔗 URL: ${web_url}`
}] };
}
```
**Multi-Action - Structured**:
```typescript
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'close',
message: 'Issue closed successfully',
issue: { /* key fields */ }
}, null, 2) }] };
```
**Complex - Custom**:
```typescript
return { content: [{ type: "text", text: JSON.stringify({
action: "updated",
discussion_id: "...",
updated_note: {...}
}) }] };
```
### Error Handling Pattern
```typescript
try {
// Operation logic
} catch (e) {
// Simple CRUD
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Error: ${String(e)}`
}) }] };
// Multi-Action
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error: ${String(e)}`
}, null, 2) }] };
}
```
---
## Tool Comparison Table
| Feature | Simple CRUD | Multi-Action | Complex |
|---------|-------------|--------------|---------|
| projectname param | ✅ Optional | ✅ Optional | ❌ May use projectId |
| format param | ✅ Required | ❌ Not used | ❌ Not used |
| action enum | ❌ Not used | ✅ Required | ❌ Custom |
| Emoji output | ✅ Concise format | ❌ Not used | ❌ Not used |
| HTML cleaning | ✅ Always | ✅ Always | ⚠️ If applicable |
| Response type | JSON or text | Structured JSON | Custom |
| Direct fetch API | ❌ Use service | ✅ Often used | ✅ If needed |
| Complexity | Low | Medium | High |
---
**All examples follow the project's standardized patterns and conventions from CLAUDE.md!**

View File

@@ -0,0 +1,158 @@
---
name: rxswift-memory-check
description: Quick RxSwift memory leak detection for iOS. Finds missing dispose bags, retain cycles, and strong self references. Use when debugging memory issues, checking Observable subscriptions, or investigating retain cycles in RxSwift code.
allowed-tools: Read, Grep, Glob
---
# RxSwift Memory Leak Detector
Fast, focused check for RxSwift memory management issues in iOS code.
## When to Activate
- "memory leak", "retain cycle", "dispose bag"
- "RxSwift memory issues", "check subscriptions"
- "[weak self]", "memory management"
- Debugging memory problems or crashes
## Quick Check Process
### Step 1: Find RxSwift Subscriptions
Use Grep to locate all Observable subscriptions:
- Pattern: `\.subscribe\(`
- Check surrounding code for proper disposal
### Step 2: Verify Each Subscription
For every `.subscribe(`:
1. ✅ Has `.disposed(by: disposeBag)`
2. ✅ Uses `[weak self]` or `[unowned self]` in closures
3. ✅ DisposeBag is a property, not local variable
### Step 3: Check Common Patterns
#### 🔴 Pattern 1: Missing Disposal
```swift
viewModel.data
.subscribe(onNext: { data in })
// MISSING: .disposed(by: disposeBag)
```
#### 🔴 Pattern 2: Retain Cycle
```swift
viewModel.data
.subscribe(onNext: { data in
self.updateUI(data) // Strong self!
})
```
#### 🔴 Pattern 3: Local DisposeBag
```swift
func loadData() {
let disposeBag = DisposeBag() // Local variable!
// Cancels immediately when function ends
}
```
### Step 4: Generate Report
Focused report with:
- Critical issues by severity
- File locations and line numbers
- Current vs. fixed code
- Impact assessment
- Recommended fixes
## Search Patterns
### Find subscriptions without disposal
```
Pattern: \.subscribe\(
Context: Check next 5 lines for .disposed
```
### Find strong self references
```
Pattern: subscribe.*\{[^[]*self\.
Context: -A 3 -B 1
```
### Find local DisposeBag declarations
```
Pattern: let disposeBag = DisposeBag\(\)
```
## Output Format
```markdown
# RxSwift Memory Check Report
## Critical Issues: X
### 1. Missing Disposal - MEMORY LEAK
**File**: `PaymentViewModel.swift:45`
**Risk**: Memory accumulation, eventual crash
**Current**:
```swift
// Missing disposal
```
**Fix**:
```swift
.disposed(by: disposeBag)
```
**Impact**: [Explanation]
---
## Summary
🔴 Critical: X (memory leaks/retain cycles)
⚠️ Warnings: X (could use weak self)
## Files Status
✅ Clean files
⚠️ Files with warnings
🔴 Files with critical issues
```
## DisposeBag Best Practices
✅ **Correct**: Property-level DisposeBag
```swift
class ViewModel {
private let disposeBag = DisposeBag()
}
```
**Wrong**: Local DisposeBag
```swift
func loadData() {
let disposeBag = DisposeBag() // Cancels immediately!
}
```
## When to Use `weak` vs `unowned`
- **Default**: Always use `[weak self]` (safer)
- **Rare**: Use `[unowned self]` only if 100% sure self outlives subscription
## Quick Fix Guide
1. **Add Missing Disposal**: `.disposed(by: disposeBag)`
2. **Add Weak Self**: `[weak self]` in closure
3. **Move DisposeBag**: To property level
## Testing the Fix
Suggest verification:
1. Run Instruments with Leaks template
2. Navigate to/from screens multiple times
3. Check Debug Memory Graph for cycles
4. Verify view controllers deallocate
## Reference
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.

View File

@@ -0,0 +1,536 @@
# RxSwift Memory Check Examples
Detailed examples of memory leaks, retain cycles, and proper RxSwift memory management.
## Critical Issue Examples
### Example 1: Missing Disposal (Memory Leak)
#### ❌ Problem Code
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
func processPayment() {
paymentUC.execute(amount: paymentAmount.value)
.subscribe(onNext: { [weak self] result in
self?.handleResult(result)
})
// MISSING: .disposed(by: disposeBag)
}
}
```
**Problem**: Subscription never releases, accumulates in memory
**Impact**: Memory grows over time, eventually crashes app
**Symptoms**: Increasing memory usage, app slowdown
#### ✅ Fixed Code
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
func processPayment() {
paymentUC.execute(amount: paymentAmount.value)
.subscribe(onNext: { [weak self] result in
self?.handleResult(result)
})
.disposed(by: disposeBag) // Added
}
}
```
**Fix**: Add `.disposed(by: disposeBag)` to every subscription
**Result**: Subscription properly cleaned up when ViewModel deallocates
---
### Example 2: Retain Cycle (Strong Self Reference)
#### ❌ Problem Code
```swift
class StoresViewModel: BaseViewModel<StoresState> {
private let storesUC: StoresUseCase
private let disposeBag = DisposeBag()
let stores = BehaviorRelay<[Store]>(value: [])
func loadStores() {
storesUC.getStores()
.subscribe(onNext: { stores in
// Strong self reference - retain cycle!
self.stores.accept(stores)
})
.disposed(by: disposeBag)
}
}
```
**Problem**: Closure captures self strongly, creating retain cycle
**Impact**: ViewModel never deallocates, memory leak
**Symptoms**: View controllers don't deallocate, memory grows
**Detection**: Xcode Debug Memory Graph shows cycle
#### ✅ Fixed Code
```swift
class StoresViewModel: BaseViewModel<StoresState> {
private let storesUC: StoresUseCase
private let disposeBag = DisposeBag()
let stores = BehaviorRelay<[Store]>(value: [])
func loadStores() {
storesUC.getStores()
.subscribe(onNext: { [weak self] stores in
// Weak self - no retain cycle
self?.stores.accept(stores)
})
.disposed(by: disposeBag)
}
}
```
**Fix**: Use `[weak self]` in closure capture list
**Result**: ViewModel can deallocate properly, no memory leak
---
### Example 3: Local DisposeBag (Early Cancellation)
#### ❌ Problem Code
```swift
class TransactionViewModel: BaseViewModel<TransactionState> {
func loadTransactions() {
let disposeBag = DisposeBag() // Local variable!
transactionUC.getTransactions()
.subscribe(onNext: { [weak self] transactions in
self?.handleTransactions(transactions)
})
.disposed(by: disposeBag)
// disposeBag deallocates here, cancels subscription immediately!
}
}
```
**Problem**: DisposeBag deallocates when function exits, canceling subscription
**Impact**: Observable never completes, callbacks never fire
**Symptoms**: Data doesn't load, UI doesn't update
#### ✅ Fixed Code
```swift
class TransactionViewModel: BaseViewModel<TransactionState> {
private let disposeBag = DisposeBag() // Property
func loadTransactions() {
transactionUC.getTransactions()
.subscribe(onNext: { [weak self] transactions in
self?.handleTransactions(transactions)
})
.disposed(by: disposeBag) // Uses property
}
}
```
**Fix**: Make DisposeBag a class property, not local variable
**Result**: Subscription lives as long as the ViewModel
---
### Example 4: Multiple DisposeBags (Anti-Pattern)
#### ❌ Problem Code
```swift
class DashboardViewModel: BaseViewModel<DashboardState> {
private var searchDisposeBag = DisposeBag() // Anti-pattern
private var dataDisposeBag = DisposeBag() // Anti-pattern
private var uiDisposeBag = DisposeBag() // Anti-pattern
func search(query: String) {
searchService.search(query)
.subscribe(onNext: { results in })
.disposed(by: searchDisposeBag)
}
func loadData() {
dataService.loadData()
.subscribe(onNext: { data in })
.disposed(by: dataDisposeBag)
}
}
```
**Problem**: Unnecessary complexity, harder to manage subscriptions
**Impact**: Confusing code, potential for errors
#### ✅ Fixed Code
```swift
class DashboardViewModel: BaseViewModel<DashboardState> {
private let disposeBag = DisposeBag() // Single DisposeBag
func search(query: String) {
searchService.search(query)
.subscribe(onNext: { results in })
.disposed(by: disposeBag) // Same bag
}
func loadData() {
dataService.loadData()
.subscribe(onNext: { data in })
.disposed(by: disposeBag) // Same bag
}
}
```
**Fix**: Use single DisposeBag per class
**Result**: Simpler code, all subscriptions disposed together
---
## Complete Examples
### Example 5: Proper RxSwift Memory Management
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
// Dependencies
private let paymentUC: PaymentUseCase
private let validationService: ValidationService
// DisposeBag property (not local!)
private let disposeBag = DisposeBag()
// State
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessing = BehaviorRelay<Bool>(value: false)
init(paymentUC: PaymentUseCase, validationService: ValidationService) {
self.paymentUC = paymentUC
self.validationService = validationService
super.init()
setupBindings()
}
private func setupBindings() {
// Proper disposal with weak self
paymentAmount
.map { [weak self] amount in
self?.validationService.validateAmount(amount) ?? false
}
.subscribe(onNext: { [weak self] isValid in
self?.setState(isValid ? .valid : .invalid)
})
.disposed(by: disposeBag)
}
func processPayment() {
isProcessing.accept(true)
paymentUC.execute(amount: paymentAmount.value)
.subscribeOn(ConcurrentScheduler.background)
.observeOn(MainScheduler.instance)
.subscribe(
onNext: { [weak self] result in
self?.handleSuccess(result)
},
onError: { [weak self] error in
self?.handleError(error)
},
onCompleted: { [weak self] in
self?.isProcessing.accept(false)
}
)
.disposed(by: disposeBag) // Always disposed
}
private func handleSuccess(_ result: PaymentResult) {
setState(.success(result))
}
private func handleError(_ error: Error) {
setState(.error(error))
isProcessing.accept(false)
}
}
```
**Key Points**:
- ✅ DisposeBag is a property
- ✅ Every subscription uses `.disposed(by:)`
- ✅ Every closure uses `[weak self]`
- ✅ Proper error handling
- ✅ Correct scheduler usage
---
### Example 6: ViewController with Proper Bindings
```swift
class PaymentViewController: UIViewController {
@IBOutlet weak var amountTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton!
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
private let viewModel: PaymentViewModel
private let disposeBag = DisposeBag() // Property
init(viewModel: PaymentViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use dependency injection")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
}
private func setupUI() {
title = "Payment"
}
private func bindViewModel() {
// Input: TextField ViewModel
amountTextField.rx.text
.orEmpty
.bind(to: viewModel.paymentAmount)
.disposed(by: disposeBag) // Disposed
// Input: Button tap ViewModel action
confirmButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.processPayment()
})
.disposed(by: disposeBag) // Disposed
// Output: ViewModel UI
viewModel.isProcessing
.bind(to: loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag) // Disposed
viewModel.isProcessing
.map { !$0 }
.bind(to: confirmButton.rx.isEnabled)
.disposed(by: disposeBag) // Disposed
// State handling
viewModel.getState()
.compactMap { $0 }
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] state in
self?.handleState(state.name)
})
.disposed(by: disposeBag) // Disposed
}
private func handleState(_ state: PaymentState) {
switch state {
case .success(let result):
showSuccessAlert(result)
case .error(let error):
showErrorAlert(error)
default:
break
}
}
}
```
**Key Points**:
- ✅ Single DisposeBag for all bindings
- ✅ All UI bindings properly disposed
- ✅ Weak self in subscribe closures
- ✅ Clean separation of concerns
---
## Common Scenarios
### Scenario 1: Timer/Interval Observable
#### ❌ Problem
```swift
func startTimer() {
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] tick in
self?.updateTime(tick)
})
// No disposal - timer runs forever!
}
```
#### ✅ Solution
```swift
class TimerViewModel {
private let disposeBag = DisposeBag()
func startTimer() {
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] tick in
self?.updateTime(tick)
})
.disposed(by: disposeBag) // Stops when ViewModel deallocates
}
}
```
---
### Scenario 2: Network Requests
#### ❌ Problem
```swift
func loadData() {
networkService.fetchData()
.subscribe(onNext: { data in
self.data = data // Strong self
})
// No disposal
}
```
#### ✅ Solution
```swift
class DataViewModel {
private let disposeBag = DisposeBag()
func loadData() {
networkService.fetchData()
.subscribe(
onNext: { [weak self] data in
self?.data = data
},
onError: { [weak self] error in
self?.handleError(error)
}
)
.disposed(by: disposeBag)
}
}
```
---
### Scenario 3: Chained Observables
#### ❌ Problem
```swift
func processData() {
fetchData()
.flatMap { data in
return self.transform(data) // Strong self
}
.flatMap { transformed in
return self.save(transformed) // Strong self
}
.subscribe(onNext: { result in
self.handleResult(result) // Strong self
})
// No disposal
}
```
#### ✅ Solution
```swift
class DataProcessor {
private let disposeBag = DisposeBag()
func processData() {
fetchData()
.flatMap { [weak self] data -> Observable<TransformedData> in
guard let self = self else { return .empty() }
return self.transform(data)
}
.flatMap { [weak self] transformed -> Observable<Result> in
guard let self = self else { return .empty() }
return self.save(transformed)
}
.subscribe(
onNext: { [weak self] result in
self?.handleResult(result)
},
onError: { [weak self] error in
self?.handleError(error)
}
)
.disposed(by: disposeBag)
}
}
```
---
## Detection Tools
### Using Xcode Debug Memory Graph
1. Run app in Simulator/Device
2. Navigate to screen with suspected leak
3. Pop back
4. Xcode → Debug → View Memory Graph
5. Look for objects that should have deallocated
6. Check retain cycle graph
### Using Instruments
1. Product → Profile
2. Choose "Leaks" template
3. Record while using app
4. Navigate between screens
5. Check for red leak indicators
6. Inspect stack traces
### Manual Verification
Add `deinit` to ViewModels and ViewControllers:
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
deinit {
print("✅ PaymentViewModel deallocated") // Should print when leaving screen
}
}
class PaymentViewController: UIViewController {
deinit {
print("✅ PaymentViewController deallocated") // Should print when popping
}
}
```
If `deinit` doesn't print, you have a memory leak!
---
## Quick Reference
### Memory Management Checklist
```markdown
## RxSwift Memory Check
For each Observable subscription:
- [ ] Has `.disposed(by: disposeBag)`
- [ ] Uses `[weak self]` in closures
- [ ] DisposeBag is a property (not local)
- [ ] No strong reference cycles
- [ ] Error handling present
- [ ] deinit prints when tested
```
### Common Patterns
| Pattern | Issue | Fix |
|---------|-------|-----|
| Missing disposal | Memory leak | Add `.disposed(by: disposeBag)` |
| Strong self | Retain cycle | Use `[weak self]` |
| Local DisposeBag | Early cancel | Make it a property |
| Multiple bags | Complexity | Use single DisposeBag |
| No error handling | Crashes | Add `onError` handler |

View File

@@ -0,0 +1,301 @@
---
name: skill-builder
description: Create new Claude Code agent skills. Automatically analyzes requirements and generates complete skill with proper structure, triggers, and tools. Use when you want to "create a skill", "build a new skill", "make a skill for", or automate repetitive tasks.
allowed-tools: Read, Write, Glob, Bash
---
# Skill Builder
Autonomously create new Claude Code agent skills with intelligent analysis and generation.
## When to Activate
- "create a skill for...", "build a new skill"
- "make a skill that...", "I need a skill to..."
- "generate a skill for..."
## Autonomous Creation Process
### Step 1: Analyze User Request
**Automatically infer from the request:**
1. **Task Type Detection**:
- Review/Check → Code Review Skill (Read, Grep, Glob)
- Generate/Create → Code Generator (Read, Write, Glob)
- Analyze/Report → Analyzer (Read, Grep, Glob, Bash)
- Refactor/Improve → Refactoring Assistant (Read, Write, Grep, Glob)
- Test → Test Assistant (Read, Write, Bash)
- Document → Documentation Generator (Read, Write, Glob)
2. **Skill Name Derivation**:
- Extract key technology/concept from request
- Format: `[technology]-[action]` (e.g., "check iOS performance" → `ios-performance-check`)
- Keep lowercase with hyphens, max 64 chars
3. **Trigger Keywords Extraction**:
- Parse user's language for natural phrases
- Add common variations and synonyms
- Include file type mentions if applicable
4. **Tool Requirements**:
- Read-only tasks → `Read, Grep, Glob`
- Generation tasks → `Read, Write, Glob`
- Command/build tasks → `Read, Write, Bash`
- Complex workflows → No restrictions
5. **Output Format Selection**:
- Review/Check → Report with issues and fixes
- Generate → File creation confirmation with examples
- Analyze → Metrics report with visualizations
- Refactor → Proposal with before/after
- Test → Test results and coverage report
- Document → Generated documentation preview
### Step 2: Smart Question Strategy
**Only ask if truly ambiguous:**
- Multiple valid approaches? → Ask which approach
- Unclear scope? → Ask for scope clarification
- Critical choices? → Ask for user preference
**Never ask if inferable:**
- Skill name → Auto-generate from request
- Basic tools → Auto-select based on task type
- Common triggers → Auto-generate from context
- Output format → Auto-select based on skill type
### Step 3: Auto-Generate Skill Structure
**Automatically create complete skill with:**
#### Auto-Generated Skill Name
Format: `[technology]-[action]`
Examples:
- "check iOS naming" → `ios-naming-check`
- "generate React component" → `react-component-generator`
- "analyze dependencies" → `dependency-analyzer`
#### Auto-Generated Description
Template:
```
[Verb] [what] [context]. [Specifics]. Use when [trigger1], [trigger2], "[quoted phrases]", or [patterns].
```
Auto-include:
- Specific action extracted from request
- What it checks/generates/analyzes
- Trigger phrases derived from user's language
- File types if mentioned
- Natural language variations
#### Auto-Selected Tools
Based on detected task type:
- Review/Check → `Read, Grep, Glob`
- Generate → `Read, Write, Glob`
- Analyze with commands → `Read, Grep, Glob, Bash`
- Refactor → `Read, Write, Grep, Glob`
- Test with execution → `Read, Write, Bash`
#### Auto-Generated Content Structure
Select appropriate template based on task type (from templates.md):
- Code Review → Template 1
- Code Generator → Template 2
- Analyzer/Reporter → Template 3
- Refactoring Assistant → Template 4
- Test Assistant → Template 5
- Documentation Generator → Template 6
Populate template with:
- Auto-generated name, description, tools
- Task-specific process steps
- Relevant output format
- Examples from similar skills (from examples.md)
### Step 4: Generate and Validate
**Auto-create directory structure:**
```bash
mkdir -p .claude/skills/[auto-generated-name]
```
**Auto-generate files:**
1. **`SKILL.md`** (Required, Concise)
- Frontmatter (name, description, allowed-tools)
- When to Activate (trigger phrases)
- Process steps (core workflow)
- Output format (template structure)
- Keep under 150 lines - core instructions only!
2. **`templates.md`** (Optional, for code/structure templates)
- Code templates
- File structure templates
- Boilerplate examples
- Use when skill generates code or files
3. **`examples.md`** (Optional, for detailed examples)
- Real-world usage examples
- Before/after code samples
- Complex scenarios
- Use when skill needs detailed guidance
4. **`standards.md`** (Optional, for rules/guidelines)
- Coding standards
- Naming conventions
- Best practices
- Reference documentation
- Use when skill enforces specific rules
**Separation principle:**
- SKILL.md = Concise instructions (what to do, how to do it)
- Separate files = Supporting details (templates, examples, references)
**Auto-validate:**
- ✓ Valid YAML frontmatter with `---` delimiters
- ✓ Name lowercase with hyphens
- ✓ Description specific with quoted triggers
- ✓ Tools appropriate for task type
- ✓ Process steps clear and actionable
- ✓ SKILL.md concise (under 150 lines)
- ✓ Extra content moved to separate files
### Step 5: Present and Test
**Show user:**
```markdown
✅ Created skill: [name]
📁 Location: `.claude/skills/[name]/SKILL.md`
🎯 Try these phrases:
- "[trigger phrase 1]"
- "[trigger phrase 2]"
- "[trigger phrase 3]"
📖 Description: [generated description]
```
## Quick Reference: Task Types
| Type | Tools | Output | Example Name |
|------|-------|--------|--------------|
| Review | Read, Grep, Glob | Issues report | `security-review` |
| Generator | Read, Write, Glob | New files | `component-generator` |
| Analyzer | Read, Grep, Glob, Bash | Metrics report | `dependency-analyzer` |
| Refactor | Read, Write, Grep, Glob | Modified files | `extract-method` |
| Test | Read, Write, Bash | Test results | `test-runner` |
| Document | Read, Write, Glob | Documentation | `api-docs-generator` |
## Auto-Generation Examples
### Example 1: User says "create a skill to check TODO comments"
**Auto-analysis:**
- Task type: Review (check/find)
- Name: `todo-finder`
- Tools: `Read, Grep, Glob`
- Triggers: "find todos", "check todos", "show todos", "list fixmes"
- Output: Report organized by priority
**Action:** Auto-generate complete skill, create files, show confirmation
### Example 2: User says "I need to generate React components"
**Auto-analysis:**
- Task type: Generator (create/generate)
- Name: `react-component-generator`
- Tools: `Read, Write, Glob`
- Triggers: "create component", "generate component", "new component"
- Output: Component files with tests
**Action:** Auto-generate complete skill, create files, show confirmation
### Example 3: User says "make a skill for iOS performance issues"
**Auto-analysis:**
- Task type: Analyzer (check performance)
- Name: `ios-performance-check`
- Tools: `Read, Grep, Glob, Bash`
- Triggers: "check performance", "performance issues", "slow code"
- Output: Performance report with fixes
**Action:** Auto-generate complete skill, create files, show confirmation
## Standard Workflow
When user requests a skill:
1. **Analyze** request → Detect task type, extract key concepts
2. **Generate** skill name, description, triggers automatically
3. **Select** appropriate template and tools
4. **Create** `.claude/skills/[name]/SKILL.md` with complete content
5. **Validate** frontmatter, structure, triggers
6. **Present** summary with test phrases
**No questions asked unless truly ambiguous!**
## Creation Output Format
After auto-generating skill, show:
```markdown
✅ Skill Created: [name]
📁 Files created:
- `.claude/skills/[name]/SKILL.md` (core instructions)
- `.claude/skills/[name]/templates.md` (if applicable)
- `.claude/skills/[name]/examples.md` (if applicable)
- `.claude/skills/[name]/standards.md` (if applicable)
🔧 Type: [task-type]
🛠️ Tools: [tool-list]
📄 Lines: [line-count] (SKILL.md is concise!)
🎯 Test with these phrases:
- "[natural trigger 1]"
- "[natural trigger 2]"
- "[natural trigger 3]"
📖 Full description:
[Generated description with all triggers]
✅ Ready to use! Try one of the test phrases above.
```
## Internal References
Use these for generation (don't show to user):
- **templates.md**: 6 skill templates for different task types
- **examples.md**: 5 complete real-world examples
## Key Principles
1. **Be autonomous**: Infer everything possible from the user's request
2. **Ask minimally**: Only ask if genuinely ambiguous (approach, scope, critical choices)
3. **Generate completely**: Create full SKILL.md with all sections
4. **Validate automatically**: Check frontmatter, structure, triggers before presenting
5. **Present clearly**: Show what was created and how to test it
## Important Rules
### Auto-Generation
- **Auto-generate** skill name from request (lowercase-with-hyphens)
- **Auto-detect** task type to select template and tools
- **Auto-extract** trigger phrases from user's language
- **Auto-create** description with specific triggers in quotes
- **Auto-select** appropriate tools based on task type
### Quality Standards
- **No generic names**: Always use specific technology/action names
- **No vague descriptions**: Always include specific triggers and file types
- **Valid YAML**: Always use `---` delimiters and proper frontmatter format
### File Organization
- **SKILL.md must be concise**: Under 150 lines, core instructions only
- **Separate templates**: Move code templates to `templates.md`
- **Separate examples**: Move detailed examples to `examples.md`
- **Separate standards**: Move rules/guidelines to `standards.md`
- **Clear separation**: Instructions in SKILL.md, details in other files
---
**Ready for autonomous skill generation!** Just tell me what skill you need.