# Conversation Feature Architecture Modernization Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Refactor the conversation/create-memory flow to a clearer Android architecture built around `Compose + UDF + ViewModel + Repository`, with domain orchestration extracted out of the current giant ViewModel. **Architecture:** Keep the app in a single Android module for now, but reorganize the conversation feature internally as `presentation/`, `domain/`, and `data/`. Replace the current `CreateMemoryViewModel` orchestration with a feature-scoped repository, a `ConversationSessionCoordinator` for realtime/session logic, and a `VoiceMessageOrchestrator` for recording/upload logic. Keep existing network ports/adapters as the transport boundary; do not add Hilt or new Gradle modules in this plan. **Tech Stack:** Kotlin, Jetpack Compose, Android ViewModel, StateFlow, Coroutines, Room DAOs, existing `ConversationApiPort` / `ConversationRealtimePort`, JUnit4, `kotlinx-coroutines-test`, `mockito-inline`. **Implementation Rules:** - Use @test-driven-development for every task. - Use @verification-before-completion before every commit. - Do not introduce Hilt or multi-module Gradle changes in this plan. - Treat this as the pilot feature; memoir/payment/profile can adopt the same pattern after this is stable. --- ### Task 1: Introduce Conversation Presentation Contracts **Files:** - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/ConversationMessageUi.kt` - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryUiState.kt` - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryAction.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryUiStateTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun default_state_represents_a_new_idle_conversation() { val state = CreateMemoryUiState() assertNull(state.conversationId) assertEquals(CreateMemoryUiState.Connection.Disconnected, state.connection) assertTrue(state.messages.isEmpty()) assertFalse(state.isSending) assertFalse(state.isRecording) assertNull(state.errorMessage) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.presentation.CreateMemoryUiStateTest` Expected: FAIL with unresolved references for `CreateMemoryUiState`. **Step 3: Write the minimal implementation** ```kotlin data class ConversationMessageUi( val id: String, val text: String, val sender: Sender, val timestamp: Long, val isStreaming: Boolean = false, ) { enum class Sender { USER, ASSISTANT, SYSTEM } } data class CreateMemoryUiState( val conversationId: String? = null, val messages: List = emptyList(), val draft: String = "", val transcript: String = "", val connection: Connection = Connection.Disconnected, val isSending: Boolean = false, val isRecording: Boolean = false, val recordingDurationSeconds: Int = 0, val isProcessing: Boolean = false, val processingMessage: String? = null, val errorMessage: String? = null, ) { enum class Connection { Disconnected, Preparing, Connecting, Connected } } sealed interface CreateMemoryAction { data class DraftChanged(val value: String) : CreateMemoryAction data object SendText : CreateMemoryAction data object StartRecording : CreateMemoryAction data object StopRecording : CreateMemoryAction data object CancelGeneration : CreateMemoryAction data class Initialize(val conversationId: String?) : CreateMemoryAction } ``` **Step 4: Run the test to verify it passes** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.presentation.CreateMemoryUiStateTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/ConversationMessageUi.kt \ app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryUiState.kt \ app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryAction.kt \ app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/presentation/CreateMemoryUiStateTest.kt git commit -m "refactor: add conversation presentation contracts" ``` ### Task 2: Create a Feature-Scoped Conversation Repository **Files:** - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/domain/ConversationRepository.kt` - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationRemoteDataSource.kt` - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationLocalDataSource.kt` - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationRepositoryImpl.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/data/ConversationRepositoryImplTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun sync_messages_fetches_remote_and_persists_locally() = runTest { val remote = FakeConversationRemoteDataSource( messages = listOf(messageDto(id = "m1", conversationId = "c1")) ) val local = FakeConversationLocalDataSource() val repository = ConversationRepositoryImpl(remote, local) repository.syncMessages("c1") assertEquals(listOf("c1"), remote.requestedMessageConversationIds) assertEquals(listOf("m1"), local.insertedMessages.flatten().map { it.id }) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.data.ConversationRepositoryImplTest` Expected: FAIL because the repository and data source classes do not exist. **Step 3: Write the minimal implementation** ```kotlin interface ConversationRepository { fun observeConversations(): Flow> fun observeMessages(conversationId: String): Flow> suspend fun syncConversations() suspend fun syncMessages(conversationId: String) suspend fun createConversation(): Result suspend fun deleteConversation(conversationId: String): Result } class ConversationRemoteDataSource( private val conversationApi: ConversationApiPort, ) { suspend fun createConversation() = conversationApi.createConversation() suspend fun getConversationList() = conversationApi.getConversationList() suspend fun getMessages(conversationId: String) = conversationApi.getMessages(conversationId) suspend fun deleteConversation(conversationId: String) = conversationApi.deleteConversation(conversationId) } class ConversationLocalDataSource( private val conversationDao: ConversationDao, private val messageDao: MessageDao, private val segmentDao: ConversationSegmentDao, ) { fun observeConversations() = conversationDao.getAllConversations() fun observeMessages(conversationId: String) = messageDao.getMessagesByConversationId(conversationId) suspend fun insertConversations(items: List) = items.forEach(conversationDao::insertConversation) suspend fun insertMessages(items: List) = messageDao.insertMessages(items) suspend fun getConversation(id: String) = conversationDao.getConversationById(id) suspend fun deleteConversation(entity: Conversation) = conversationDao.deleteConversation(entity) suspend fun latestEmptyConversation() = conversationDao.getLatestEmptyConversation() suspend fun deleteOtherEmptyConversations(keepId: String) = conversationDao.deleteOtherEmptyConversations(keepId) } class ConversationRepositoryImpl( private val remote: ConversationRemoteDataSource, private val local: ConversationLocalDataSource, ) : ConversationRepository { // Port existing create/sync/delete logic out of the old repositories here. } ``` Map `MessageDto -> Message` and `ConversationListItemDto/CreateConversationResponse -> Conversation` inside `ConversationRepositoryImpl`. **Step 4: Run the test to verify it passes** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.data.ConversationRepositoryImplTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/domain/ConversationRepository.kt \ app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationRemoteDataSource.kt \ app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationLocalDataSource.kt \ app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/data/ConversationRepositoryImpl.kt \ app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/data/ConversationRepositoryImplTest.kt git commit -m "refactor: add conversation feature repository" ``` ### Task 3: Extract Conversation Session Orchestration **Files:** - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/domain/ConversationSessionCoordinator.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/domain/ConversationSessionCoordinatorTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun initialize_existing_conversation_prepares_loads_history_and_connects() = runTest { val repository = FakeConversationRepository(messages = listOf(message(id = "m1"))) val realtime = FakeConversationRealtimePort() val coordinator = ConversationSessionCoordinator( repository = repository, realtime = realtime, accessTokenProvider = { "token-1" }, ) coordinator.initializeExistingConversation("conversation-1") advanceUntilIdle() assertEquals(listOf("prepare", "connect:conversation-1:token-1"), realtime.calls) assertEquals("conversation-1", coordinator.state.value.conversationId) assertEquals(1, coordinator.state.value.messages.size) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.domain.ConversationSessionCoordinatorTest` Expected: FAIL because `ConversationSessionCoordinator` does not exist. **Step 3: Write the minimal implementation** ```kotlin data class ConversationSessionState( val conversationId: String? = null, val messages: List = emptyList(), val connection: CreateMemoryUiState.Connection = CreateMemoryUiState.Connection.Disconnected, ) class ConversationSessionCoordinator( private val repository: ConversationRepository, private val realtime: ConversationRealtimePort, private val accessTokenProvider: suspend () -> String?, ) { private val _state = MutableStateFlow(ConversationSessionState()) val state: StateFlow = _state.asStateFlow() suspend fun initializeExistingConversation(conversationId: String) { _state.value = _state.value.copy( conversationId = conversationId, connection = CreateMemoryUiState.Connection.Preparing, ) realtime.prepare() repository.syncMessages(conversationId) val token = accessTokenProvider() _state.value = _state.value.copy( connection = CreateMemoryUiState.Connection.Connecting, messages = repository.observeMessages(conversationId).first(), ) realtime.connect(conversationId, token, onMessage = ::handleIncomingMessage) } suspend fun startNewConversation() { /* create via repository + connect */ } suspend fun sendText(text: String) { /* delegate to realtime */ } suspend fun endConversation() { /* delegate to realtime */ } private fun handleIncomingMessage(message: WebSocketMessage) { /* minimal state updates */ } } ``` Keep the first implementation intentionally narrow: existing conversation init, new conversation start, send text, end conversation, and basic incoming message handling for transcript/agent text/connect/error. **Step 4: Run the test to verify it passes** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.domain.ConversationSessionCoordinatorTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/domain/ConversationSessionCoordinator.kt \ app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/domain/ConversationSessionCoordinatorTest.kt git commit -m "refactor: extract conversation session coordinator" ``` ### Task 4: Extract Voice Recording and Upload Orchestration **Files:** - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/voice/VoiceMessageOrchestrator.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/feature/voice/VoiceMessageOrchestratorTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun stop_and_send_recording_dispatches_segments_in_order() = runTest { val recordingCoordinator = FakeRecordingCoordinator( finishedResult = finishedRecording( voiceSessionId = "voice-1", files = listOf(segment(index = 0), segment(index = 1)), ) ) val sessionCoordinator = FakeConversationSessionCoordinator(conversationId = "conversation-1") val orchestrator = VoiceMessageOrchestrator( recordingCoordinator = recordingCoordinator, sessionCoordinator = sessionCoordinator, ) orchestrator.stopAndSendRecording() assertEquals(listOf(0, 1), sessionCoordinator.sentSegmentIndexes) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.voice.VoiceMessageOrchestratorTest` Expected: FAIL because `VoiceMessageOrchestrator` does not exist. **Step 3: Write the minimal implementation** ```kotlin class VoiceMessageOrchestrator( private val recordingCoordinator: RecordingCoordinator, private val sessionCoordinator: ConversationSessionCoordinator, ) { suspend fun startRecording(): RecordingStartResult = recordingCoordinator.startRecording() suspend fun stopAndSendRecording(recordingId: String? = null): RecordingFinishResult { val finishResult = recordingCoordinator.finishRecording(recordingId) if (finishResult is RecordingFinishResult.Success) { finishResult.segmentFiles.forEach { file -> sessionCoordinator.sendAudioSegment( voiceSessionId = finishResult.voiceSessionId, segment = file, ) } } return finishResult } suspend fun cancelRecording(): RecordingCancelResult = recordingCoordinator.cancelRecording() } ``` Move only recording/segment dispatch and retry scheduling into this class. Leave audio playback UI state in the ViewModel for now. **Step 4: Run the test to verify it passes** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.voice.VoiceMessageOrchestratorTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/feature/voice/VoiceMessageOrchestrator.kt \ app-android/app/src/test/java/com/huaga/life_echo/feature/voice/VoiceMessageOrchestratorTest.kt git commit -m "refactor: extract voice message orchestrator" ``` ### Task 5: Rebuild CreateMemoryViewModel Around UiState and Actions **Files:** - Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt` - Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelTest.kt` - Modify: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt` - Modify: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun send_text_action_updates_ui_state_and_delegates_to_session() = runTest { val sessionCoordinator = FakeConversationSessionCoordinator(conversationId = "conversation-1") val viewModel = CreateMemoryViewModel( sessionCoordinator = sessionCoordinator, voiceOrchestrator = FakeVoiceMessageOrchestrator(), ) viewModel.onAction(CreateMemoryAction.DraftChanged("hello")) viewModel.onAction(CreateMemoryAction.SendText) advanceUntilIdle() assertEquals("", viewModel.uiState.value.draft) assertEquals(listOf("hello"), sessionCoordinator.sentTexts) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelWarmupTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelRecordingCoordinatorTest` Expected: FAIL because the ViewModel still exposes dozens of mutable flows and does not support `uiState`/`onAction`. **Step 3: Write the minimal implementation** ```kotlin class CreateMemoryViewModel( private val sessionCoordinator: ConversationSessionCoordinator, private val voiceOrchestrator: VoiceMessageOrchestrator, ) : ViewModel() { private val _uiState = MutableStateFlow(CreateMemoryUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun onAction(action: CreateMemoryAction) { when (action) { is CreateMemoryAction.DraftChanged -> { _uiState.update { it.copy(draft = action.value) } } CreateMemoryAction.SendText -> viewModelScope.launch { val text = _uiState.value.draft.trim() if (text.isBlank()) return@launch sessionCoordinator.sendText(text) _uiState.update { it.copy(draft = "", isSending = false) } } is CreateMemoryAction.Initialize -> viewModelScope.launch { action.conversationId?.let(sessionCoordinator::initializeExistingConversation) ?: sessionCoordinator.startNewConversation() } CreateMemoryAction.StartRecording -> viewModelScope.launch { voiceOrchestrator.startRecording() } CreateMemoryAction.StopRecording -> viewModelScope.launch { voiceOrchestrator.stopAndSendRecording() } CreateMemoryAction.CancelGeneration -> viewModelScope.launch { sessionCoordinator.cancelGeneration() } } } } ``` In `CreateMemoryScreen`, replace the many `collectAsState()` calls with one `val uiState by viewModel.uiState.collectAsState()`. Read `uiState.messages`, `uiState.connection`, `uiState.draft`, `uiState.isRecording`, etc., and dispatch UI events via `viewModel.onAction(...)`. **Step 4: Run the tests to verify they pass** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelWarmupTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelRecordingCoordinatorTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt \ app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt \ app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelTest.kt \ app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt \ app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt git commit -m "refactor: migrate create memory flow to ui state" ``` ### Task 6: Move Conversation List to the Same Feature Repository Pattern **Files:** - Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/ConversationListUiState.kt` - Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ConversationListViewModel.kt` - Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt` - Test: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/ConversationListViewModelTest.kt` **Step 1: Write the failing test** ```kotlin @Test fun refresh_conversations_updates_ui_state_from_repository() = runTest { val repository = FakeConversationRepository( conversations = flowOf(listOf(conversation(id = "c1", title = "One"))) ) val viewModel = ConversationListViewModel(repository) viewModel.refreshConversations() advanceUntilIdle() assertEquals(listOf("c1"), viewModel.uiState.value.conversations.map { it.id }) assertTrue(viewModel.uiState.value.hasLoadedInitialConversations) } ``` **Step 2: Run the test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.ConversationListViewModelTest` Expected: FAIL because the ViewModel does not expose `uiState`. **Step 3: Write the minimal implementation** ```kotlin data class ConversationListUiState( val conversations: List = emptyList(), val isLoading: Boolean = false, val errorMessage: String? = null, val hasLoadedInitialConversations: Boolean = false, ) class ConversationListViewModel( private val repository: ConversationRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(ConversationListUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { viewModelScope.launch { repository.observeConversations().collect { items -> _uiState.update { it.copy(conversations = items) } } } refreshConversations() } fun refreshConversations() { /* set loading, call repository.syncConversations(), update state */ } } ``` Update `ConversationListScreen` to consume `uiState` instead of separate `conversations`, `isLoading`, `error`, and `hasLoadedInitialConversations` flows. **Step 4: Run the test to verify it passes** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.ConversationListViewModelTest` Expected: PASS **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/presentation/ConversationListUiState.kt \ app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ConversationListViewModel.kt \ app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt \ app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/ConversationListViewModelTest.kt git commit -m "refactor: migrate conversation list to ui state" ``` ### Task 7: Wire the New Feature Components and Remove Legacy Repositories **Files:** - Modify: `app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt` - Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt` - Delete: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/ConversationRepository.kt` - Delete: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/MessageRepository.kt` - Modify: `docs/plans/2026-03-12-conversation-architecture-modernization.md` **Step 1: Write the failing verification test** Add one smoke-style test that proves `CreateMemoryViewModel` no longer requires the deleted repositories: ```kotlin @Test fun create_memory_view_model_accepts_feature_components_only() { val constructor = CreateMemoryViewModel::class.primaryConstructor!! val parameterTypes = constructor.parameters.mapNotNull { it.type.classifier as? KClass<*> } assertFalse(parameterTypes.contains(com.huaga.life_echo.data.repository.ConversationRepository::class)) assertFalse(parameterTypes.contains(com.huaga.life_echo.data.repository.MessageRepository::class)) } ``` Test file: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelConstructorTest.kt` **Step 2: Run the verification test to verify it fails** Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelConstructorTest` Expected: FAIL because the old repositories are still present in the constructor and wiring. **Step 3: Write the minimal implementation** In `AppContainer`, construct and expose: ```kotlin val conversationRemoteDataSource by lazy { ConversationRemoteDataSource(conversationApi) } val conversationLocalDataSource by lazy { ConversationLocalDataSource( conversationDao = database.conversationDao(), messageDao = database.messageDao(), segmentDao = database.conversationSegmentDao(), ) } val conversationRepository: com.huaga.life_echo.feature.conversation.domain.ConversationRepository by lazy { ConversationRepositoryImpl(conversationRemoteDataSource, conversationLocalDataSource) } val conversationSessionCoordinator by lazy { ConversationSessionCoordinator( repository = conversationRepository, realtime = ConversationRealtimeAdapter(webSocketClient), accessTokenProvider = TokenManager::getAccessToken, ) } ``` Then update `ViewModelFactory` to inject `conversationRepository`, `conversationSessionCoordinator`, and `VoiceMessageOrchestrator`. Delete the legacy `data/repository/ConversationRepository.kt` and `MessageRepository.kt` once there are no remaining imports. **Step 4: Run focused and full verification** Run: ```bash ./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelConstructorTest ./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelTest --tests com.huaga.life_echo.ui.viewmodel.ConversationListViewModelTest ./gradlew :app:testDebugUnitTest git diff --check rg -n "data\\.repository\\.(ConversationRepository|MessageRepository)" app-android/app/src/main/java ``` Expected: - all tests PASS - `git diff --check` prints nothing - `rg` finds no remaining imports of the deleted repositories in production code **Step 5: Commit** ```bash git add app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt \ app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt \ app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelConstructorTest.kt \ docs/plans/2026-03-12-conversation-architecture-modernization.md git add -u app-android/app/src/main/java/com/huaga/life_echo/data/repository git commit -m "refactor: finish conversation architecture modernization" ``` ### Task 8: Write the Follow-Up Architecture Note **Files:** - Create: `docs/plans/2026-03-12-conversation-architecture-followups.md` **Step 1: Write the failing checklist** Write a markdown checklist covering the next phases that are explicitly out of scope for this plan: ```markdown - [ ] Move auth/token state behind an injected session abstraction - [ ] Split `ApiService` into feature remote data sources for memoir/payment/profile - [ ] Introduce Hilt once feature boundaries stop moving - [ ] Evaluate feature-first Gradle modules after the conversation pilot stabilizes ``` **Step 2: Save the checklist** Create `docs/plans/2026-03-12-conversation-architecture-followups.md` Expected: file exists with the checklist. **Step 3: Keep it intentionally short** Do not add implementation detail. The point is to freeze the next-phase scope, not start a second refactor. **Step 4: Verify the file exists and is tracked** Run: ```bash test -f docs/plans/2026-03-12-conversation-architecture-followups.md git status --short docs/plans/2026-03-12-conversation-architecture-followups.md ``` Expected: - `test` exits `0` - `git status` shows the file as added or staged **Step 5: Commit** ```bash git add docs/plans/2026-03-12-conversation-architecture-followups.md git commit -m "docs: capture conversation architecture follow-ups" ```