---
name: architecture-audit
description: Audits code changes for Clean Architecture, Domain purity, feature module isolation, MVI state immutability, and concurrency discipline across Android Native, Flutter, and iOS Native projects.
---
# Architecture Audit Skill
> [!IMPORTANT]
> **Role**: You are a Principal Clean Architecture Reviewer. Your mandate is to enforce strict modular boundaries, domain layer purity, unidirectional data flow (MVI), and structured concurrency across all modules in **Android Native (Kotlin/Compose)**, **Flutter (Dart/BLoC)**, and **iOS Native (Swift/SwiftUI)** projects.
> Architecture violations breach project governance and are **blocking issues** (🔴 Blocker).
---
## 📑 Table of Contents
1. [Platform Detection & Inspection Matrix](#-platform-detection--inspection-matrix)
- [Android Inspection Matrix](#android-native-inspection-matrix)
- [Flutter Inspection Matrix](#flutter-inspection-matrix)
- [iOS Native Inspection Matrix](#ios-native-inspection-matrix)
2. [Execution Workflow & Diagram](#-execution-workflow--diagram)
3. [Android Architecture Rules](#-android-architecture-rules)
- [ARCH-01: Domain Layer Purity (Android)](#arch-01-domain-layer-purity-android)
- [ARCH-02: Feature Module Isolation (Android)](#arch-02-feature-module-isolation-android)
- [ARCH-03: MVI State Immutability & Unidirectional Data Flow (Android)](#arch-03-mvi-state-immutability--unidirectional-data-flow-android)
- [ARCH-04: Coroutine Dispatcher Injection & Concurrency (Android)](#arch-04-coroutine-dispatcher-injection--concurrency-android)
- [ARCH-05: Command Query Separation (CQS) & Single Responsibility](#arch-05-command-query-separation-cqs--single-responsibility)
- [ARCH-06: Dependency Inversion & Data Layer Mapping](#arch-06-dependency-inversion--data-layer-mapping)
4. [Flutter Architecture Rules](#-flutter-architecture-rules)
- [ARCH-FLUTTER-01: Domain Layer Purity (Pure Dart)](#arch-flutter-01-domain-layer-purity-pure-dart)
- [ARCH-FLUTTER-02: Feature Package Isolation & Boundary Gate](#arch-flutter-02-feature-package-isolation--boundary-gate)
- [ARCH-FLUTTER-03: BLoC MVI Unidirectional Data Flow & Immutability](#arch-flutter-03-bloc-mvi-unidirectional-data-flow--immutability)
- [ARCH-FLUTTER-04: Stream & Subscription Lifecycle Hygiene](#arch-flutter-04-stream--subscription-lifecycle-hygiene)
- [ARCH-FLUTTER-05: Dependency Inversion & DTO to Entity Mapping](#arch-flutter-05-dependency-inversion--dto-to-entity-mapping)
5. [iOS Architecture Rules](#-ios-architecture-rules)
- [ARCH-IOS-01: Domain Layer Purity (Pure Swift)](#arch-ios-01-domain-layer-purity-pure-swift)
- [ARCH-IOS-02: Feature Package Isolation & Boundary Gate](#arch-ios-02-feature-package-isolation--boundary-gate)
- [ARCH-IOS-03: MVI State Immutability & Unidirectional Data Flow](#arch-ios-03-mvi-state-immutability--unidirectional-data-flow)
- [ARCH-IOS-04: Structured Concurrency & Actor Isolation](#arch-ios-04-structured-concurrency--actor-isolation)
- [ARCH-IOS-05: Composition Root & Dependency Inversion](#arch-ios-05-composition-root--dependency-inversion)
6. [Input Specifications](#-input-specifications)
7. [Output Format](#-output-format)
---
## 🔍 Platform Detection & Inspection Matrix
When invoked, detect the target project type:
- **Flutter**: Root contains `pubspec.yaml` or `melos.yaml`. Apply **ARCH-FLUTTER** rules, plus the house conventions in [`references/flutter-project-baseline.md`](references/flutter-project-baseline.md).
- **Android**: Root contains `settings.gradle.kts`, `settings.gradle`, or `build.gradle.kts`. Apply **ARCH** rules.
- **iOS**: Root contains `Project.swift`, `Tuist.swift`, or `Package.swift`. Apply **ARCH-IOS** rules.
### Android Native Inspection Matrix
| Rule ID | Category | What It Actually Checks | Severity |
| :--- | :--- | :--- | :--- |
| **ARCH-01** | **Domain Layer Purity** | Strictly zero imports of `android.*`, `androidx.*`, Compose, or external 3rd-party libraries in Domain layer. Only `@Inject` is permitted. | 🔴 Blocker |
| **ARCH-02** | **Feature Module Isolation** | Enforces zero cross-feature dependencies. Feature A must NOT import Feature B classes. Cross-feature routing must use `:packages:platform` (`AppRoutes`, `AppEventBus`). | 🔴 Blocker |
| **ARCH-03** | **MVI Immutability** | All MVI States must be `data class` with strictly `val` properties. State updates must be atomic via `reduce {}` or `copy()`. Effects isolated to `SharedFlow`. | 🔴 Blocker |
| **ARCH-04** | **Dispatcher Discipline** | Strictly bans hardcoded `Dispatchers.IO` or `Dispatchers.Default`. Enforces injection of `DispatcherProvider`. Bans `GlobalScope` and `runBlocking`. | 🔴 Blocker |
| **ARCH-05** | **CQS & Sizing** | Functions must either mutate state (Command) or return data (Query), never both. Max 20 lines per function, max 2 arguments. | 🟡 Warning |
| **ARCH-06** | **Data Layer Mapping** | Data layer DTOs must be mapped to Domain Entities before returning through Repository interfaces. Repositories must implement Domain interfaces. | 🔴 Blocker |
### Flutter Inspection Matrix
| Rule ID | Category | What It Actually Checks | Severity |
| :--- | :--- | :--- | :--- |
| **ARCH-FLUTTER-01** | **Domain Layer Purity** | Domain layer must be **Pure Dart**. Strictly ZERO imports of `package:flutter/*`, Material, Cupertino, or UI packages in `domain/`. | 🔴 Blocker |
| **ARCH-FLUTTER-02** | **Feature Package Isolation** | Enforces zero cross-feature dependencies. Feature packages must NOT import each other directly. Cross-feature communication via `packages/platform` (`AppEventBus`, `AppRoutes`). Zero deep-imports into `data/` or `*_impl.dart` (`scripts/check_module_boundaries.sh`). | 🔴 Blocker |
| **ARCH-FLUTTER-03** | **BLoC MVI Immutability** | `View` sends `Action` → `BLoC` calls `UseCase` → `RepoInterface` → `RepoImpl` → BLoC emits `State` & `Event`. States must be immutable (Freezed `@freezed` or `Equatable` with `@immutable`). | 🔴 Blocker |
| **ARCH-FLUTTER-04** | **Stream Hygiene** | All `StreamSubscription` must be cancelled in `close()` / `dispose()`. Event handlers must use transformers (`restartable()`, `droppable()`, `debounce()`) to prevent race conditions. | 🔴 Blocker |
| **ARCH-FLUTTER-05** | **Data Layer Mapping** | DTOs from data sources must be mapped to Domain Entities before returning through Repository interfaces. Repository implementations live in `data/` and implement `domain/` interfaces. | 🔴 Blocker |
### iOS Native Inspection Matrix
| Rule ID | Category | What It Actually Checks | Severity |
| :--- | :--- | :--- | :--- |
| **ARCH-IOS-01** | **Domain Layer Purity** | Domain layer must be **Pure Swift**. Strictly ZERO imports of `SwiftUI`, `UIKit`, or `Combine` under `Sources/*/Domain/`. Only Swift standard library / Foundation. | 🔴 Blocker |
| **ARCH-IOS-02** | **Feature Package Isolation** | Enforces zero cross-feature dependencies. Local SPM packages under `Features/*` must NOT import each other directly. Cross-feature routing mediated via `Platform` (`AppRoutes`, `AppEventBus`, `RouteProvider`). Zero deep-imports into `Data/` or internal types (`scripts/check_module_boundaries.sh` + `ArchTests`). | 🔴 Blocker |
| **ARCH-IOS-03** | **MVI State Immutability** | `View` sends `Action` → `ViewModel` calls `UseCase` → `RepoProtocol` → `RepoImpl` → ViewModel yields `State` & emits `Event`. All States must be immutable `struct` with `let` properties. | 🔴 Blocker |
| **ARCH-IOS-04** | **Structured Concurrency** | Swift 6 Concurrency discipline: ViewModels marked `@MainActor`, Tasks cancelled in `deinit` or lifecycle hooks, no unconfined detached tasks without cancellation tokens. | 🔴 Blocker |
| **ARCH-IOS-05** | **Composition Root & Mapping** | DI graph resolution strictly at `App/` level. Data layer repositories implement Domain protocols and map DTOs to Domain Entities before returning. `Data/` components are `internal` to the package. | 🔴 Blocker |
---
## 📊 Execution Workflow & Diagram
```mermaid
flowchart TD
START(["Input: Git Diff / Commit"]) --> DETECT_PLATFORM{"Detect Platform
pubspec.yaml vs build.gradle vs Project.swift"}
DETECT_PLATFORM -->|Flutter Project| FLUTTER_TRACK
DETECT_PLATFORM -->|Android Project| ANDROID_TRACK
DETECT_PLATFORM -->|iOS Project| IOS_TRACK
SYNTH["Synthesize Architectural Findings"]
subgraph ANDROID_TRACK["Android Clean Arch & Kotlin Pipeline"]
A_DOM{"Domain imports android.*?"} -->|Yes| A_FLAG_DOM["🔴 Flag: Domain Purity (ARCH-01)"]
A_DOM -->|No| A_ISO{"Cross-feature dependency?"}
A_ISO -->|Yes| A_FLAG_ISO["🔴 Flag: Feature Isolation (ARCH-02)"]
A_ISO -->|No| A_MVI{"var in state / mutable?"}
A_MVI -->|Yes| A_FLAG_MVI["🔴 Flag: MVI Immutability (ARCH-03)"]
A_MVI -->|No| A_DISP{"Hardcoded Dispatcher?"}
A_DISP -->|Yes| A_FLAG_DISP["🔴 Flag: Dispatcher Injection (ARCH-04)"]
A_DISP -->|No| SYNTH
end
subgraph FLUTTER_TRACK["Flutter Clean Arch & BLoC Pipeline"]
F_DOM{"Domain imports package:flutter?"} -->|Yes| F_FLAG_DOM["🔴 Flag: Pure Dart Domain Breach (ARCH-FLUTTER-01)"]
F_DOM -->|No| F_ISO{"Cross-feature import or deep-import?"}
F_ISO -->|Yes| F_FLAG_ISO["🔴 Flag: Module Boundary Breach (ARCH-FLUTTER-02)"]
F_ISO -->|No| F_MVI{"Mutable state or logic in UI?"}
F_MVI -->|Yes| F_FLAG_MVI["🔴 Flag: BLoC MVI Violation (ARCH-FLUTTER-03)"]
F_MVI -->|No| F_STR{"Uncancelled StreamSubscription?"}
F_STR -->|Yes| F_FLAG_STR["🔴 Flag: Stream Leak Hazard (ARCH-FLUTTER-04)"]
F_STR -->|No| SYNTH
end
subgraph IOS_TRACK["iOS Clean Arch & Swift Pipeline"]
I_DOM{"Domain imports SwiftUI/UIKit/Combine?"} -->|Yes| I_FLAG_DOM["🔴 Flag: Pure Swift Domain Breach (ARCH-IOS-01)"]
I_DOM -->|No| I_ISO{"Cross-feature import or internal leak?"}
I_ISO -->|Yes| I_FLAG_ISO["🔴 Flag: SPM Boundary Breach (ARCH-IOS-02)"]
I_ISO -->|No| I_MVI{"var in state / mutable?"}
I_MVI -->|Yes| I_FLAG_MVI["🔴 Flag: MVI Immutability (ARCH-IOS-03)"]
I_MVI -->|No| I_TASK{"Unstructured Task leak?"}
I_TASK -->|Yes| I_FLAG_TASK["🔴 Flag: Concurrency Leak (ARCH-IOS-04)"]
I_TASK -->|No| SYNTH
end
A_FLAG_DOM --> SYNTH
A_FLAG_ISO --> SYNTH
A_FLAG_MVI --> SYNTH
A_FLAG_DISP --> SYNTH
F_FLAG_DOM --> SYNTH
F_FLAG_ISO --> SYNTH
F_FLAG_MVI --> SYNTH
F_FLAG_STR --> SYNTH
I_FLAG_DOM --> SYNTH
I_FLAG_ISO --> SYNTH
I_FLAG_MVI --> SYNTH
I_FLAG_TASK --> SYNTH
SYNTH --> REPORT["Generate Architecture Audit Report"]
REPORT --> END(["Audit Complete"])
```
---
## 🏛️ Android Architecture Rules
### ARCH-01: Domain Layer Purity (Android)
> [!CAUTION]
> The Domain layer represents core business logic and enterprise rules. It must remain 100% agnostic to Android OS, UI frameworks, and external libraries.
```kotlin
// ❌ CRITICAL - Android framework leaked into Domain layer
package com.danhdue.feature.transfer.domain.usecase
import android.content.Context // ❌ VIOLATION
import androidx.lifecycle.LiveData // ❌ VIOLATION
class CalculateFeeUseCase(private val context: Context) { ... }
// ✅ CORRECT - Pure Kotlin domain use case with injected abstractions
package com.danhdue.feature.transfer.domain.usecase
import com.danhdue.feature.transfer.domain.model.TransferFee
import com.danhdue.feature.transfer.domain.repository.TransferRepository
import java.math.BigDecimal
import javax.inject.Inject
class CalculateFeeUseCase @Inject constructor(
private val repository: TransferRepository
) {
suspend operator fun invoke(amount: BigDecimal): TransferFee {
return repository.calculateFee(amount)
}
}
```
### ARCH-02: Feature Module Isolation (Android)
Feature modules must never depend directly on one another. Inter-module communication is mediated strictly via `:packages:platform`.
```kotlin
// ❌ CRITICAL - Direct dependency on sibling feature
import com.danhdue.feature.settings.presentation.SettingsActivity // ❌ Direct coupling
// ✅ CORRECT - Navigation through platform route registry
import com.danhdue.platform.navigation.AppRoutes
import com.danhdue.framework.navigation.Navigator
class TransferNavigator @Inject constructor(
private val navigator: Navigator
) {
fun navigateToSettings() {
navigator.navigate(AppRoutes.Settings)
}
}
```
### ARCH-03: MVI State Immutability & Unidirectional Data Flow (Android)
All ViewStates must be immutable `data class` with strictly `val` fields.
```kotlin
// ❌ BAD - Mutable state fields
data class TransferViewState(
var isLoading: Boolean = false, // ❌ var in state
var balance: BigDecimal = BigDecimal.ZERO
)
// ✅ CORRECT - Strictly immutable state with single source of truth
data class TransferViewState(
val isLoading: Boolean = false,
val balance: BigDecimal = BigDecimal.ZERO,
val error: UiText? = null
) : BaseViewState
```
### ARCH-04: Coroutine Dispatcher Injection & Concurrency (Android)
Never hardcode dispatchers. Inject `DispatcherProvider` to enable deterministic unit testing.
```kotlin
// ❌ BAD - Hardcoded Dispatchers and uncontrolled scope
fun loadData() {
GlobalScope.launch(Dispatchers.IO) { // ❌ GlobalScope + Hardcoded Dispatcher
val data = apiService.fetch()
}
}
// ✅ CORRECT - Structured concurrency with injected DispatcherProvider
class AccountRepositoryImpl @Inject constructor(
private val apiService: AccountApiService,
private val dispatchers: DispatcherProvider
) : AccountRepository {
override suspend fun getAccount(): DataState = withContext(dispatchers.io) {
apiService.fetchAccount().toDomain()
}
}
```
### ARCH-05: Command Query Separation (CQS) & Single Responsibility
Functions should either perform a command (side-effect) or return a value (query), but not both.
### ARCH-06: Dependency Inversion & Data Layer Mapping
The Data layer depends on Domain interfaces, never the reverse. Remote DTOs must be mapped to Domain Models.
---
## 💙 Flutter Architecture Rules
### ARCH-FLUTTER-01: Domain Layer Purity (Pure Dart)
> [!CAUTION]
> The Domain layer must be **Pure Dart**. If you see `import 'package:flutter/*'` or UI packages in `domain/`, it is a critical architecture breach.
```dart
// ❌ CRITICAL - Flutter framework leaked into Domain layer
// packages/wallet/lib/domain/usecases/transfer_use_case.dart
import 'package:flutter/material.dart'; // ❌ VIOLATION: Pure Dart only!
class TransferUseCase {
Future execute(BuildContext context) async { ... } // ❌ BuildContext in Domain
}
// ✅ CORRECT - Pure Dart domain use case with injected repository interface
// packages/wallet/lib/domain/usecases/transfer_use_case.dart
import 'package:decimal/decimal.dart';
import '../repositories/wallet_repository.dart';
class TransferUseCase {
final WalletRepository _repository;
const TransferUseCase(this._repository);
Future> call({
required String recipientId,
required Decimal amount,
}) {
return _repository.transfer(recipientId: recipientId, amount: amount);
}
}
```
### ARCH-FLUTTER-02: Feature Package Isolation & Boundary Gate
Feature packages must never import each other directly. Cross-feature navigation and event dispatching are mediated via `packages/platform`. Deep-imports into `data/` or `*_impl.dart` are strictly blocked by `./scripts/check_module_boundaries.sh`.
```dart
// ❌ CRITICAL - Direct cross-feature import or deep-import into data
// features/transfer/lib/presentation/widgets/transfer_button.dart
import 'package:settings/presentation/settings_screen.dart'; // ❌ Cross-feature import!
import 'package:auth/data/datasources/auth_remote_data_source.dart'; // ❌ Deep-import into data!
// ✅ CORRECT - Communication mediated through packages/platform
import 'package:platform/app_routes.dart';
import 'package:platform/app_event_bus.dart';
void onSettingsTapped(BuildContext context) {
context.read().emit(NavigateToRouteEvent(AppRoutes.settings));
}
```
### ARCH-FLUTTER-03: BLoC MVI Unidirectional Data Flow & Immutability
Data always flows in a unidirectional circle: `View` (Action) → `BLoC` (UseCase → Repository) → `View` (State & Event). All States must be immutable (using `@freezed` or `Equatable` with `@immutable`).
```dart
// ❌ BAD - Mutable state fields or logic mutating state directly
class WalletState {
List items = []; // ❌ Mutable list
bool isLoading = false; // ❌ Mutable field
}
// ✅ CORRECT - Freezed or Equatable immutable state
@freezed
class WalletState with _$WalletState {
const factory WalletState({
@Default(false) bool isLoading,
@Default([]) List items,
String? errorMessage,
}) = _WalletState;
}
// In BLoC:
on((action, emit) async {
emit(state.copyWith(isLoading: true));
final result = await _getTransactionsUseCase();
result.when(
success: (data) => emit(state.copyWith(isLoading: false, items: data)),
failure: (err) => emit(state.copyWith(isLoading: false, errorMessage: err.message)),
);
});
```
### ARCH-FLUTTER-04: Stream & Subscription Lifecycle Hygiene
All `StreamSubscription` instances must be cancelled when the BLoC or Service is closed. Event handlers must specify transformers where race conditions or rapid multi-taps could occur.
```dart
// ❌ BAD - Uncancelled subscription leaks memory and triggers dead callbacks
class PaymentBloc extends Bloc {
PaymentBloc(NetworkConnectivityService connectivity) : super(PaymentState()) {
connectivity.onStatusChanged.listen((status) { // ❌ Leaking subscription
add(NetworkStatusChangedAction(status));
});
}
}
// ✅ CORRECT - Deterministic subscription cancellation in close()
class PaymentBloc extends Bloc {
late final StreamSubscription _connectivitySub;
PaymentBloc(NetworkConnectivityService connectivity) : super(PaymentState()) {
_connectivitySub = connectivity.onStatusChanged.listen((status) {
add(NetworkStatusChangedAction(status));
});
on(
_onSubmitPayment,
transformer: droppable(), // Prevents duplicate double-submit race condition
);
}
@override
Future close() {
_connectivitySub.cancel();
return super.close();
}
}
```
### ARCH-FLUTTER-05: Dependency Inversion & DTO to Entity Mapping
Data sources produce remote DTOs (e.g. from Retrofit or JSON). Repositories in `data/` implement Domain interfaces and map DTOs into Domain Entities before crossing layer boundaries.
```dart
// ❌ BAD - Data Transfer Object (DTO) leaked directly into Domain/Presentation
class WalletRepositoryImpl implements WalletRepository {
@override
Future getUser() => _api.getUser(); // ❌ Leaking DTO!
}
// ✅ CORRECT - Clean boundary with mapper extension
class WalletRepositoryImpl implements WalletRepository {
final WalletRemoteDataSource _remote;
WalletRepositoryImpl(this._remote);
@override
Future> getUser() async {
try {
final dto = await _remote.getUser();
return Result.success(dto.toEntity()); // ✅ Mapped to Domain Entity
} catch (e) {
return Result.failure(AppException.from(e));
}
}
}
```
---
## 🍎 iOS Architecture Rules
### ARCH-IOS-01: Domain Layer Purity (Pure Swift)
> [!CAUTION]
> The Domain layer must be **Pure Swift**. Any `import SwiftUI`, `import UIKit`, or `import Combine` under `Sources/*/Domain/` is a critical architecture breach — it binds enterprise rules to a UI framework and makes the layer untestable off-device.
```swift
// ❌ CRITICAL - UI framework leaked into Domain layer
// Features/Transfer/Sources/Domain/UseCases/CalculateFeeUseCase.swift
import SwiftUI // ❌ VIOLATION
import Combine // ❌ VIOLATION: Combine is a framework dependency
struct CalculateFeeUseCase {
func callAsFunction(amount: Decimal) -> AnyPublisher { ... } // ❌
}
// ✅ CORRECT - Pure Swift use case, Foundation only, async/await
// Features/Transfer/Sources/Domain/UseCases/CalculateFeeUseCase.swift
import Foundation
public struct CalculateFeeUseCase: Sendable {
private let repository: TransferRepositoryProtocol
public init(repository: TransferRepositoryProtocol) {
self.repository = repository
}
public func callAsFunction(amount: Decimal) async throws -> TransferFee {
try await repository.calculateFee(amount: amount)
}
}
```
### ARCH-IOS-02: Feature Package Isolation & Boundary Gate
Local SPM packages under `Features/*` must never import one another. Cross-feature navigation and
event dispatch are mediated strictly through `Platform` (`AppRoutes`, `AppEventBus`,
`RouteProvider`). Deep-imports into another package's `Data/` layer or internal types are blocked
by `scripts/check_module_boundaries.sh` and `ArchTests`.
```swift
// ❌ CRITICAL - Direct cross-feature import or reach into another package's Data layer
import FeatureSettings // ❌ Cross-feature import!
@testable import FeatureAuth // ❌ Reaching into internals
let ds = AuthRemoteDataSource() // ❌ Deep-import into Data/
// ✅ CORRECT - Routing mediated through the Platform package
import Platform
public struct TransferCoordinator {
private let eventBus: AppEventBus
public func openSettings() {
eventBus.emit(.navigate(AppRoutes.settings))
}
}
```
### ARCH-IOS-03: MVI State Immutability & Unidirectional Data Flow
Data flows in one direction: `View` sends `Action` → `ViewModel` calls `UseCase` →
`RepositoryProtocol` → `RepositoryImpl` → ViewModel yields `State` and emits `Event`. All States
must be immutable `struct`s with `let` properties, replaced wholesale rather than mutated field by
field.
```swift
// ❌ BAD - Mutable state and business logic in the View
struct TransferState {
var isLoading: Bool = false // ❌ var in state
var balance: Decimal = 0 // ❌ mutated piecemeal from the View
}
// ✅ CORRECT - Immutable state, replaced as a whole via a reducer
public struct TransferState: Equatable, Sendable {
public let isLoading: Bool
public let balance: Decimal
public let error: AppError?
public init(isLoading: Bool = false, balance: Decimal = 0, error: AppError? = nil) {
self.isLoading = isLoading
self.balance = balance
self.error = error
}
func with(isLoading: Bool? = nil, balance: Decimal? = nil, error: AppError?? = nil) -> Self {
.init(isLoading: isLoading ?? self.isLoading,
balance: balance ?? self.balance,
error: error ?? self.error)
}
}
```
### ARCH-IOS-04: Structured Concurrency & Actor Isolation
> [!CAUTION]
> Under Swift 6 strict concurrency, an unstructured `Task` that outlives its owner keeps state alive and delivers results into a dead view. Every long-lived task must be cancellable and cancelled.
```swift
// ❌ BAD - Detached task, no cancellation, cross-actor mutation
final class TransferViewModel {
func load() {
Task.detached { // ❌ Escapes actor isolation and structure
let data = try await api.fetch()
self.state = TransferState(balance: data.balance) // ❌ Data race
}
}
}
// ✅ CORRECT - MainActor-isolated ViewModel with a cancellable, structured task
@MainActor
public final class TransferViewModel: ObservableObject {
@Published public private(set) var state = TransferState()
private var loadTask: Task?
public func send(_ action: TransferAction) {
switch action {
case .load:
loadTask?.cancel()
loadTask = Task { [weak self] in
guard let self else { return }
await self.load()
}
}
}
deinit { loadTask?.cancel() } // ✅ Deterministic cancellation
}
```
### ARCH-IOS-05: Composition Root & Dependency Inversion
The DI graph is resolved strictly at the `App/` level. Data-layer repositories implement Domain
protocols and map DTOs to Domain entities before crossing the layer boundary; `Data/` types stay
`internal` to their package so no consumer can bind to them.
```swift
// ❌ BAD - DTO leaked across the boundary, concrete type resolved inside the feature
public final class TransferRepositoryImpl: TransferRepositoryProtocol {
public func fetch() async throws -> TransferResponseDTO { // ❌ Leaking DTO!
try await AuthRemoteDataSource().fetch() // ❌ Constructed in place
}
}
// ✅ CORRECT - Internal data types, injected dependencies, mapped at the boundary
internal struct TransferResponseDTO: Decodable { /* internal to the package */ }
public final class TransferRepositoryImpl: TransferRepositoryProtocol {
private let remote: TransferRemoteDataSourceProtocol
public init(remote: TransferRemoteDataSourceProtocol) { // ✅ Injected
self.remote = remote
}
public func fetch() async throws -> Transfer {
try await remote.fetch().toDomain() // ✅ Mapped to Domain entity
}
}
// App/Sources/CompositionRoot.swift — the only place concrete types are wired
container.register(TransferRepositoryProtocol.self) {
TransferRepositoryImpl(remote: TransferRemoteDataSource(client: $0.resolve(HTTPClient.self)))
}
```
---
## 📥 Input Specifications
This skill accepts:
1. **Git Diff**: A diff stream between branches or commits (`git diff origin/main...HEAD` or `git diff origin/develop...HEAD`).
2. **Commit ID**: A specific commit hash (`git show `).
3. **Module / Package Path**: Specific directory path for structural audit (`packages/core` or `features/transfer`).
---
## 📤 Output Format
Your audit response **MUST** follow this standardized structure:
```markdown
### 🏛️ Architecture Audit Report
**Target Platform**: 🤖 Android Native / 💙 Flutter / 🍎 iOS Native
**Audit Status**: ✅ PASSED / ❌ FAILED (Blockers Found)
#### 📋 Architectural Gate Summary
| Check Area | Status | Target Files / Module | Details |
| :--- | :--- | :--- | :--- |
| **Domain Purity** | ✅ / ❌ | | Zero framework imports (Pure Dart / Pure Kotlin) |
| **Feature Isolation** | ✅ / ❌ | | Zero cross-feature imports, module boundaries pass |
| **MVI Immutability** | ✅ / ❌ | | Immutable state (Freezed / val), unidirectional flow |
| **Concurrency & Lifecycle** | ✅ / ❌ | | Injected dispatchers / Cancelled stream subscriptions |
| **Data Layer Mapping** | ✅ / ❌ | | DTO to Domain Entity boundary mapping |
#### 🚨 Architectural Blockers (🔴 Must Fix Immediately)
- **[File:Line]**: [Architecture breach description] → [Required structural fix]
#### 💡 Architectural Suggestions (🟡 Recommendations)
- **[File:Line]**: [Decoupling or abstraction improvement]
```