in progress — Android/iOS are shell-only: 59 shared `commonMain` screens and no native duplicates. macOS mounts all 25 sidebar tabs via `KMPMacView`; the platform-parity gate covers all 38 detected shared-screen directories. Compose Desktop consumes shared screens. Remaining gaps: (1) Mac XCFramework target/re-link and runtime proof on a macOS host (`./gradlew :shared:assembleXCFramework` + Xcode Frameworks phase), (2) desktop Compose Desktop production promotion (manual `desktop-v*` tag release; Tauri remains in prod)

Spec: F78 — Compose Multiplatform: Shared UI for Android, iOS, and Mac

Current V2 shell: Lumi · Work · Business · Hub.

Status: in progress — Android/iOS are shell-only: 59 shared commonMain screens and no native duplicates. macOS mounts all 25 sidebar tabs via KMPMacView; the platform-parity gate covers all 38 detected shared-screen directories. Compose Desktop consumes shared screens. Remaining gaps: (1) Mac XCFramework target/re-link and runtime proof on a macOS host (./gradlew :shared:assembleXCFramework + Xcode Frameworks phase), (2) desktop Compose Desktop production promotion (manual desktop-v* tag release; Tauri remains in prod)

The canonical four-tab order remains Lumi · Work · Business · Hub; Pulse is a nested operating surface and Company Hub is grouped inside Hub.

Contract Graph note (2026-07-16, refreshed 2026-08-02): F78 is represented by contracts/features/F78.cue with source/docs proof for shared content and native host bridges. The spec’s in progress status and its remaining target/runtime boundary are consistent; XCFramework/macOS target and runtime gaps remain blocking.

Priority: F79 is the mobile product contract; F78 is its shared-code implementation path. Keep F78 active while KMP is chosen; explicitly deprecate it otherwise.

Relation: F78 replaces F30’s separate Android Compose/iOS SwiftUI architecture and extends F47’s Mac surface. F48 remains a Tauri web wrapper. When agreed, F78 §4 supersedes only F30/F47 technology sections.


1. Problem & CEO Job

F79 owns mobile information architecture; F78 only defines how iOS and Android share it.

Separate Android Compose and iOS SwiftUI apps duplicate screens and bugs. At 77+ features, a three-person team cannot maintain parity without cutting quality.

Kotlin Multiplatform + Compose share business logic, API clients, ViewModels, navigation, and screen content. Native shells retain iOS/Mac TabView, NavigationStack, Sheet, and Liquid Glass, plus Android Material 3. One team ships each feature and fix once while CEOs get software designed for their device, not ported from another.


2. User Stories & Acceptance Criteria

Code sharing and architecture

  • As a mobile engineer, I want every screen’s content written once in Compose, so that a bug fixed on Android is fixed everywhere automatically.

    • Each feature screen is a @Composable in shared/src/commonMain/ with zero platform imports.
    • Each screen’s ViewModel is in shared/src/commonMain/ using kotlinx-coroutines and Ktor — no Android-only or iOS-only dependencies.
    • The shared module compiles to: Android AAR, iOS framework (Kotlin/Native via XCFramework), macOS framework (same XCFramework, separate slice).
    • ./gradlew :shared:linkReleaseFrameworkIosArm64 succeeds in CI with zero errors.
    • ./gradlew :androidApp:assembleRelease succeeds in CI with zero errors.
  • As a mobile engineer, I want navigation defined once in shared code, so that the tab structure and deep-link routing are identical on every platform.

    • Compose Navigation (or Voyager/Circuit — see §12) drives all screen transitions in shared code.
    • Each platform shell bridges to shared navigation: SwiftUI on iOS/Mac mounts a ComposeUIViewController at the root; the shell’s TabView calls into shared nav via a bridging API (LumiNavController.switchTab(tab:)).
    • Deep links (lumi://today, lumi://tasks/{id}, etc.) are handled in shared code; each platform registers the URL scheme and calls LumiNavController.handleDeepLink(url:).

iOS — native shell, shared content

  • As an iPhone user, I want the iOS app to use native iOS 26 Liquid Glass chrome, so that Lumi feels like a first-class iPhone app, not an Android port.

    • The iOS target (iosApp/) hosts the F79 four-tab shell in SwiftUI: TabView, NavigationStack per tab, Sheet for modals, and .searchable for search. Shared Compose owns content.
    • Each tab’s content is rendered by a ComposeView (UIViewRepresentable wrapper around ComposeUIViewController) that hosts the corresponding shared Compose screen.
    • TabView uses iOS 26 tabBarMinimizeBehavior / glass styling when available, with a plain-Material-3 fallback for iOS 17–25.
    • Native NavigationStack handles swipe-back gesture and navigation bar chrome; the shared Compose content does NOT draw its own top bar on iOS.
    • Sheets, action sheets, and share sheets use SwiftUI presentation APIs triggered by callbacks from shared ViewModels (via a PlatformDelegate interface).
    • The iOS app passes all existing F30 acceptance criteria for every feature listed in F30 §2.
  • As an iPhone user, I want push notifications, biometric auth, and Spotlight integration to work exactly as they do in a pure SwiftUI app, so that I don’t notice the Compose layer.

    • Push notifications (F36) are registered and displayed via native UNUserNotificationCenter; tap actions route to shared LumiNavController.handleDeepLink.
    • Biometric auth uses LocalAuthentication in the SwiftUI layer; the shared ViewModel exposes a requestBiometricUnlock() callback the SwiftUI layer implements.
    • NSUserActivity / Spotlight donation is done in the SwiftUI layer after shared ViewModel signals a record was opened.

Android — Compose Material 3 shell, shared content

  • As an Android user, I want the Android app to use Material 3 with correct Android patterns (back gesture, predictive back, edge-to-edge), so that Lumi doesn’t feel like an iPhone port.
    • The Android target (androidApp/) hosts the F79 four-tab shell with Compose Material 3 NavigationBar and Compose Navigation NavHost. Shared Compose owns all content; Company Hub is grouped inside Hub.
    • Predictive back animation (BackHandler with PredictiveBackGestureOverlay) is wired in the Android shell.
    • Edge-to-edge layout with WindowCompat.setDecorFitsSystemWindows(false); Modifier .windowInsetsPadding applied at the scaffold level in shared code via expect/actual.
    • FCM push notifications registered and displayed via Android NotificationManager; tap actions route to shared LumiNavController.handleDeepLink.
    • The Android app passes all existing F30 acceptance criteria for every feature listed in F30 §2.

macOS — SwiftUI shell, all content screens from KMP

  • As a Mac user, I want a native Mac app where every content screen is the same KMP shared screen as Android and Windows/Linux, so that new features appear everywhere at once.

    • The macOS XCFramework (macosArm64 slice of shared.xcframework) exposes factory functions in macosMain/MainViewController.kt that return NSViewController instances hosting a Compose-for-macOS ComposePanel for each SharedScreen case (Today, Brief, Pulse, Tasks, Projects, Decisions, CRM, Chat, Hub, Messages, Teams, Settings, …).
    • The macOS shell is SwiftUI: sidebar navigation (NavigationSplitView for two-pane, HSplitView for three-pane e.g. CRM: list | detail | activity), menu bar extra, toolbar.
    • Each content pane is a KMPMacView (NSViewControllerRepresentable) wrapping the NSViewController returned by the macosMain factory — not ComposeUIViewController (which is iOS-only UIKit) and not a UIKit ComposeView. AppKit ComposePanel is the macOS-specific embedding API.
    • Keyboard shortcuts (⌘1–⌘9 tabs, ⌘N new task, ⌘F search, ⌘, settings) are wired in the SwiftUI shell via LumiCommands; they post NSNotification consumed by the active KMPMacView.
    • The macOS app passes all F47 acceptance criteria for Mac features.
    • ./gradlew :shared:linkReleaseMacosArm64 succeeds in CI (currently blocked — see Implementation Audit below).

    Blockers before this AC is fully active:

    1. macosArm64 is not currently configured in android/shared/build.gradle.kts, so the macosMain factories have no active target build or linked XCFramework evidence.
    2. PullToRefreshBox in commonMain is iOS-only. The macOS build fails: error: unresolved reference 'PullToRefreshBox'. Fix: add a macOS (and JVM) expect/actual that renders a no-op wrapper or scroll-triggered refresh — the pull gesture makes no sense on a trackpad but the composable must compile. Until fixed, #if canImport(shared) in SharedScreenBridge.swift remains false and the SwiftUI fallback is active.

Feature parity gate

  • As a product owner, I want a CI parity check that fails when a shared screen is missing a platform registration, so that no feature silently ships on one platform and not another.
    • scripts/check-platform-parity.sh enumerates all Screen objects in shared nav graph and verifies each is registered in the iOS TabView/nav, Android NavHost, and macOS sidebar.
    • This script runs in CI and fails the build if any platform is missing a registration.

3. Scope Boundaries

In:

  • Shared Compose UI for all feature screens (content, not chrome)
  • Shared ViewModels, repositories, API client (Ktor), and domain models
  • Shared navigation graph and deep-link handling
  • iOS SwiftUI shell (TabView, NavigationStack, Sheet, Liquid Glass)
  • Android Compose Material 3 shell (NavigationBar, NavHost)
  • macOS SwiftUI shell (sidebar, NavigationSplitView, keyboard shortcuts) + ComposePanel bridge (macosMain)
  • Windows/Linux Compose Desktop shell (:desktopApp, jvmMain) — same commonMain screens, different native window/tray/shortcut OS integration; see F48
  • CI build verification for all four targets (Android, iOS, macOS, JVM/desktop)
  • Migration path from current separate Android-Compose + iOS-SwiftUI codebases
  • PullToRefreshBox expect/actual for macosMain and jvmMain (no-op / scroll-triggered; unblocks macOS + desktop KMP builds)
  • macosMain/MainViewController.kt factory functions (NSViewController + ComposePanel) for all SharedScreen cases

Out:

  • Apple Watch (remains SwiftUI-only via WatchKit; Watch Connectivity bridge is the boundary)
  • Web frontend (React/TypeScript — separate product surface)
  • Backend (Django — unchanged)
  • Store billing wiring (StoreKit 2 / Play Billing — platform shell concern; referenced in F30/F24)
  • Re-design of any screen’s UX (this spec is architectural migration, not a redesign)

4. Platform Architecture

┌──────────────────────────────────────────────────────────────────────────────────┐
│                         shared/ (Kotlin Multiplatform)                           │
│                                                                                  │
│  commonMain:                                                                     │
│  ├─ domain/          (models, use-cases, repository interfaces)                  │
│  ├─ data/            (Ktor API client, DTOs, repository impls)                   │
│  ├─ presentation/    (ViewModels, UI state, navigation graph)                    │
│  └─ ui/              (Compose screens — content only; Today/Brief/Pulse/Tasks…)  │
│                                                                                  │
│  androidMain:  expect/actual for Android (FCM, Keystore, back gesture)          │
│  iosMain:      expect/actual for iOS (ComposeUIViewController factories)         │
│  macosMain:    expect/actual for macOS (NSViewController+ComposePanel factories) │
│  jvmMain:      expect/actual for JVM — Windows + Linux (Compose Desktop)        │
└──────┬──────────────────┬───────────────────┬────────────────────┬──────────────┘
       │                  │                   │                    │
┌──────▼──────┐  ┌────────▼───────┐  ┌───────▼────────┐  ┌───────▼──────────────┐
│ androidApp/ │  │   iosApp/      │  │  LumiMac       │  │    desktopApp/        │
│ Compose     │  │ SwiftUI shell  │  │ SwiftUI shell  │  │  Compose Desktop      │
│ shell       │  │                │  │ (ios/ target)  │  │  shell (JVM)          │
│             │  │ TabView        │  │                │  │                       │
│NavigationBar│  │NavigationStack │  │ Sidebar        │  │ Window {}             │
│ NavHost     │  │ Sheet          │  │ SplitView      │  │ System tray           │
│ M3 scaffold │  │ Liquid Glass   │  │ Toolbar        │  │ Global shortcuts      │
│ FCM         │  │ StoreKit 2     │  │ Menu bar       │  │ Autostart             │
│ Play Billing│  │ LocalAuth      │  │ Keyboard nav   │  │ Credential Manager    │
│Predictive   │  │                │  │ Spotlight      │  │ .msi / .deb installer │
│ back        │  │ KMPMacView ←──────── KMPMacView    │  │                       │
│             │  │(ComposeUIVC)   │  │(ComposePanel)  │  │                       │
└─────────────┘  └────────────────┘  └────────────────┘  └───────────────────────┘

Shell boundary rule: the shell provides chrome + OS integration only.
Every content pane is a shared Compose composable from commonMain.
Mac uses AppKit ComposePanel (macosMain); Windows/Linux use Compose Desktop Window (jvmMain).
iOS uses UIKit ComposeUIViewController (iosMain). Android uses Compose natively.

What lives where

LayerLocationRationale
API client (Ktor)shared/commonMainSingle HTTP client, single token refresh logic
DTOs + JSON (kotlinx.serialization)shared/commonMainOne schema change touches one file
Domain modelsshared/commonMainShared across all VMs and screens
Repository interfacesshared/commonMainTestable without platform
Repository implementationsshared/commonMain (using Ktor)No platform IO needed
ViewModels (ScreenModel or ViewModel)shared/commonMainState is the same on every platform
Compose screens (content)shared/commonMainRendered identically everywhere
Navigation graphshared/commonMainOne source of truth for routes/deep links
Tab chrome, nav barsPlatform shellNative fidelity — not shareable
Push notification registrationPlatform shellOS-specific APIs
Biometric authPlatform shellOS-specific APIs
Store billingPlatform shellStoreKit 2 / Play Billing
Keychain / Keystore token storageexpect/actualSame interface, OS-specific impl
Date/time formattingexpect/actual (kotlinx-datetime)Locale handling differs
File picker / image pickerexpect/actualOS-specific picker APIs

expect/actual boundaries

// shared/commonMain
expect fun platformName(): String
expect fun secureStore(): SecureStore
expect fun imagePicker(): ImagePickerLauncher
expect fun dateFormatter(pattern: String): DateFormatter

// shared/androidMain
actual fun platformName() = "Android"
actual fun secureStore(): SecureStore = AndroidKeystore()

// shared/iosMain
actual fun platformName() = "iOS"
actual fun secureStore(): SecureStore = iOSKeychain()

Compose-in-SwiftUI bridge

// iosApp/ContentView.swift
struct LumiRootView: View {
    var body: some View {
        TabView {
            ComposeView(screen: .today)
                .tabItem { Label("Today", systemImage: "sun.max") }
            ComposeView(screen: .tasks)
                .tabItem { Label("Tasks", systemImage: "checkmark.circle") }
            // ...
        }
        .tabViewStyle(.sidebarAdaptable)   // iOS 18+ / macOS
    }
}

struct ComposeView: UIViewControllerRepresentable {
    let screen: LumiScreen
    func makeUIViewController(context: Context) -> UIViewController {
        LumiKt.makeComposeViewController(screen: screen)
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

5. API Contract

No new backend API endpoints. All existing REST endpoints (defined in F01–F77 specs) are consumed by the shared Ktor client.

Shared API client (shared/data/api/LumiApiClient.kt)

ConcernImplementation
Base URLexpect val API_BASE_URL: String — prod/staging set per build variant
AuthBearer token from SecureStore; 401 → token refresh → retry once
JSONkotlinx.serialization — DTOs mirror existing DRF response shapes
TimeoutsConnect 10s, read 30s (same as web Axios config)
Certificate pinningOptional Phase 2 (not blocking MVP)

6. UI / UX

Screen-level UI is unchanged — shared Compose composables render identically to today’s Android screens. The visible difference per platform:

SurfaceAndroidiOSmacOS
Bottom navMaterial 3 NavigationBarSwiftUI TabViewSwiftUI sidebar
Top barCompose TopAppBar (hidden behind shell option)SwiftUI nav barSwiftUI toolbar
ModalsCompose ModalBottomSheetSwiftUI .sheetSwiftUI .sheet
AlertsCompose AlertDialogSwiftUI .alertSwiftUI .alert
SearchCompose SearchBarSwiftUI .searchableSwiftUI .searchable
BackPredictive back (Android 14+)Swipe-back on NavigationStackToolbar back button

i18n

The shared Compose layer uses the Compose Multiplatform compose.components.resources API (Res.string.*) for all user-facing copy in commonMain. String resource files live at:

android/shared/src/commonMain/composeResources/values/strings.xml       ← English baseline
android/shared/src/commonMain/composeResources/values-vi/strings.xml    ← Vietnamese (primary)

Access pattern in any shared composable:

import com.locuno.lumi.shared.res.Res
import org.jetbrains.compose.resources.stringResource

Text(stringResource(Res.string.brief_title))  // "Tóm tắt điều hành" in vi, "Executive Brief" in en

Rule: no hardcoded user-facing string literals in commonMain UI code (Six Rules Rule 3). Every new shared screen must add its keys to both locale XML files before merging. The Res class is generated at build time — a missing key causes a compile error, making the contract type-safe. Vietnamese is the primary locale; English is the baseline/fallback.


7. Test Plan

#CaseTypeWhere
1Shared ViewModel state transitions (Today, Tasks, Brief, Pulse…)unitshared/src/commonTest/
2API client: auth header sent, 401 triggers refresh, retry succeedsunitshared/src/commonTest/
3Repository: data flows from mock API → ViewModel → UI stateunitshared/src/commonTest/
4Navigation: deep link lumi://tasks/123 resolves to TaskDetail screenunitshared/src/commonTest/
5expect/actual SecureStore: write/read/delete token round-tripsunitshared/src/androidTest/ + iosTest/
6Android app builds (AAR + APK) without errorsbuildCI ./gradlew :androidApp:assembleRelease
7iOS XCFramework builds without errorsbuildCI ./gradlew :shared:assembleXCFramework
8macOS framework builds without errorsbuildCI ./gradlew :shared:linkReleaseMacosArm64
9iOS app compiles (xcodebuild build-for-testing)buildCI via xcodebuild
10Platform parity check: every screen registered on all platformslintCI scripts/check-platform-parity.sh
11Android E2E: critical flow (login → today → task complete)e2eEspresso / Maestro
12iOS E2E: critical flow (login → today → pulse submit)e2eXCTest UI / Maestro
13Cross-tenant: API client sends correct X-Organization-ID headerunitshared/src/commonTest/

The test plan distinguishes source/docs proof from the two remaining F78 gates: the macOS/XCFramework implementation boundary and named target-runtime/build evidence. Neither is inferred from the shared-screen inventory or collected proof nodes.


8. Current Behavior

Android: The F79 shell is active, and shared Compose now mounts Today, Brief, Pulse, all four F79 top-level roots, and the shared Hub directory composition via SharedKmpViewModel and the lane screens in android/app/src/main/kotlin/com/locuno/lumi/android/ui/LumiApp.kt. Major breadth surfaces such as chat, settings, CRM, messages, teams, approvals, decisions, commitments, and risks still remain Android-native under android/feature/**.

iOS: SwiftUI still owns the outer shell and most feature breadth. The SharedScreen route inventory lives in ios/Lumi/KMP/SharedScreenTypes.swift, while ios/Lumi/KMP/SharedScreenView.swift hosts the shared Compose controller. Together they bridge Today, Brief, Pulse, all four top-level roots, and the shared Hub directory composition into SwiftUI. Hub breadth/detail destinations, chat, settings, CRM, teams, messages, and most detail flows remain native SwiftUI screens.

macOS: ios/LumiMac/KMP/SharedScreenBridge.swift has the full KMPMacView bridge and the detected shared-screen registrations, while the #if canImport(shared) branch is currently inactive: the shared.xcframework Frameworks build phase is unlinked because macosArm64 is not configured in android/shared/build.gradle.kts. The macosMain factories are source-level adapters only until that target and the PullToRefreshBox macOS/JVM boundary are buildable. The #else native SwiftUI fallback is active for Today, Brief, and Pulse; all other screens show KMPNotLinkedView placeholders. A macOS target build, XCFramework re-link, and runtime proof are still required before claiming Mac activation.

Windows/Linux: Tauri 2 wrapper over runlumi.app — current production state. Target is Compose Desktop (:desktopApp Gradle module, jvmMain actuals) consuming the same commonMain screens as Android, iOS, and Mac. Migration per F48 and ADR-021.


9. Error States

ScenarioBehavior
Shared module fails to build for iOS targetCI blocks the PR; platform shell cannot compile without the framework
expect declaration without matching actualKotlin compiler error — cannot build
Compose rendering crash in ComposeUIViewControllerSwiftUI catches as an unhandled exception; shared error boundary Composable shows a retry card instead of crashing the tab
API timeout in shared Ktor clientViewModel emits UiState.Error("network_timeout") → screen shows retry button; i18n key common.error_network
Token refresh fails (refresh token expired)SecureStore.clearAll() → shared nav emits AuthEvent.SessionExpired → platform shell presents login screen

10. Diagrams & Docs Touched

  • DIAGRAMS.md: add KMP architecture layer to the system diagram.
  • android/AGENTS.md: update to reflect shared/ module as source of truth for ViewModels and screens; androidApp/ becomes a thin shell.
  • ios/AGENTS.md (if it exists) or frontend/AGENTS.md: add KMP bridge notes.
  • docs/specs/F30-mobile.md: add forward reference to F78 §4 for architecture; acceptance criteria are unchanged.
  • docs/specs/F47-cross-device-apple.md: replace “native SwiftUI screens” with “SwiftUI shell + ComposeView content from shared module” in the Mac sections.
  • docs/MVP_SCOPE.md: add F78 row.
  • New ADR: docs/ADR/0NN-compose-multiplatform-architecture.md — records the decision to use KMP+CMP over (a) three separate native codebases and (b) React Native / Flutter. Required before implementation begins (see §12).

11. Implementation Audit

Partial. A KMP module exists and is wired into both mobile shells, but broad feature ownership is still mixed rather than shell-only.

ItemStatusNotes
compose.components.resources wired in build.gradle.kts✅ 2026-06-22commonMain.dependencies; compose.resources { packageOfResClass } configured
composeResources/values/strings.xml baseline (en)✅ 2026-06-2215+ keys; grows as screens are localized
composeResources/values-vi/strings.xml primary (vi)✅ 2026-06-22Mirrors en key-for-key
BriefScreen.kt localized (no hardcoded strings)✅ 2026-06-22All 6 user-facing strings via Res.string.*; raw Button → LumiInlineAction
TodayScreen.kt localized (no hardcoded strings)✅ 2026-06-22All 10 user-facing strings via Res.string.*; commitments section added; raw Button → LumiInlineAction
SettingsScreen.kt localized (no hardcoded strings)✅ 2026-06-2213 settings_* keys in both en and vi; onLeaveOrg dead-code removed
CompaniesScreen.kt localized (no hardcoded strings)✅ 2026-06-228 crm_companies_* keys; company type labels (Customer/Supplier/Partner)
DealsScreen.kt localized (no hardcoded strings)✅ 2026-06-229 crm_deals_* keys; deal status mapped via when in composable
MessagesScreen.kt localized (no hardcoded strings)✅ 2026-06-225 messages_* keys
HubScreen.kt localized (no hardcoded strings)✅ 2026-06-228 hub_* keys; Pinned / Acknowledge labels localized in AnnouncementCard
WorkspaceLaneScreen.kt localized + design tokens (Rule 3 + Rule 4)✅ 2026-06-2314 new workspace_* keys (EN + VI); hardcoded strings → Res.string.*; LaneScaffold.kt deleted (stock Card + raw colorScheme.onSurfaceVariant removed); now uses LumiPageTitle+LumiSectionHeader+LumiDirectoryRow. Routing keys unchanged. See improvement file 2026-06/week-26/kmp-workspace-lane-i18n-design-tokens.md
Remaining shared screens localized☐ (guard-enforced)Keys added per-screen as touched. en↔vi parity is now mechanically guarded (2026-07-06): StringsParityTest (src/jvmTest/.../i18n/StringsParityTest.kt) fails :shared:jvmTest if values/strings.xml and values-vi/strings.xml key sets diverge, so an English fallback can no longer land silently on a VN phone. First run caught 4 drifted team_detail_* keys (present in en, missing from vi) — now translated. See improvement file 2026-07/week-27/kmp-strings-en-vi-parity.md
Form-screen migration (P30): CreateApproval ported to shared KMP (reference)✅ 2026-06-25CreateApprovalScreen + CreateApprovalScreenModel + CreateApprovalRepository in commonMain; 7 commonTest cases (all green); iOS + Mac factories makeCreateApprovalViewController(…, onCreated); .createApproval case in both SharedScreenBridge.swift files; Android entry<CreateApprovalNavKey> shadows native, model in SharedKmpViewModel; native screens/CreateApprovalScreen.kt + its WorkspaceEntry block deleted. 18 approvals_* form keys added (EN+VI). Reuses the existing shared api.createApproval/ApprovalCreateRequest. Plan + remaining 12 screens: P30
Form-screen migration (P30) batch 1: 6 create-only forms ported to shared KMP✅ 2026-06-25CreateTask, CreateProject, CreateRecurring, CreateDecision, CreateCompany, CreateDeal — each gets a dedicated Create…ScreenModel + Create…Repository in commonMain, a stateless Create…Screen (Lumi components + tokens, all strings via Res.string.*), 6–8 commonTest cases each (42 new, all green via :shared:jvmTest), iOS + Mac makeCreate…ViewController(…, onCreated) factories, .create… cases in both SharedScreenBridge.swift files, Android entry<…NavKey> + model in SharedKmpViewModel. The 6 native screens/Create*.kt files + their WorkspaceEntry blocks/imports were deleted; the native DateField helper was extracted into screens/DateField.kt so the still-native EditProjectScreen keeps compiling. Reuses existing shared create APIs/DTOs (createTask/createProject/createRecurring/createDecision/createCompany/createDeal). Date inputs are YYYY-MM-DD text fields, dropdowns are chip rows, company tags are toggle chips, CreateDecision keeps a dynamic options list. Plan: P30
Form-screen migration (P30) batch 2 (final): 6 edit/detail screens ported to shared KMP✅ 2026-06-25EditTask, EditProject (load → prefill → PUT via submit), ProjectDetail, RecurringDetail, CompanyDetail, DealDetail (load → render, inline actions kept). Each gets a dedicated …ScreenModel + …Repository in commonMain (model holds LoadState<…> for the loaded entity), a stateless …Screen (Lumi components + tokens, all strings via Res.string.*, renders Loading/Error/Success), and a commonTest model test (24 new cases, all green via :shared:jvmTest). iOS + Mac make…ViewController(…, id, onSaved/onEdit/onRan) factories; legacy .dealStatusDetail bridge cases remain as compatibility aliases for the canonical DealDetailScreen. Android: per-id models constructed inside each entry<…NavKey> from the NavKey id via id-parametrized factories on SharedKmpViewModel; native entry blocks removed from WorkspaceEntry.kt (only Subscription remains). 37 new edit/detail i18n keys (EN+VI). Reuses existing shared APIs (taskDetail/updateTask, projectDetail/updateProject, recurringDetail/runRecurringNow, companyDetail, dealDetail/updateDeal). The former duplicate status-only deal detail was removed; the workspace-api DealDetailNavKey now resolves to the canonical F50 activity-flow detail. The 6 native screens/*.kt + the now-orphaned DateField.kt were deleted. All 13 P30 screens now KMP — form-screen migration complete. Plan: P30
P30 cleanup sweep: delete orphaned feature/workspace/impl/ ViewModels + screen files left behind by P30 batch 2✅ 2026-06-2717 production files + 9 dedicated test files deleted; 3 dead test methods removed from WorkspaceTests.kt; obsolete approval/decision action-test classes removed from ActionTests.kt; WorkspaceEntry.kt (empty, migration notice comment) deleted. LumiApp.kt had already imported all screens from com.locuno.lumi.shared.ui.* — zero dangling refs after deletion. See §13 change log + improvement file 2026-06/week-04/kmp-workspace-impl-dead-code-cleanup.md
macOS blocker 1: android/shared/src/macosMain/kotlin/…/MainViewController.kt — factory functions returning NSViewController (with ComposePanel) for each SharedScreen case; mirrors iosMain/MainViewController.kt which uses ComposeUIViewController✅ 2026-07-02Initial batch (2026-06-23) wrote 31 factories; makeNotificationsViewController (F77) added 2026-06-25. 2026-06-28: 3 additional factories added (makeOrgManagementViewController, makeOrgSetupViewController, makeSubscriptionViewController). 2026-07-02: makeAuthViewController added — missed when F30 auth was migrated to shared KMP (PR #899 added it to iosMain only). macosMain ↔ iosMain factory parity now 32/32; all bridge-callable SharedScreen cases have matching macosMain symbols. See improvement files 2026-06/week-04/macos-kmp-main-view-controller.md, 2026-06/week-04/macos-kmp-notifications-factory-parity.md, 2026-06/week-04/macos-kmp-org-subscription-factory-parity.md, 2026-07/week-27/macos-kmp-auth-factory-parity.md
macOS blocker 2: PullToRefreshBox macOS+JVM expect/actual — isolate the mobile-only API from common code⏳The source split exists: Android and iOS use PullToRefreshBox; macOS no-ops; JVM enables it only on Android ART/Dalvik. TodayScreen.kt no longer imports it directly. macosArm64 is not currently configured in android/shared/build.gradle.kts, so no active macOS compile proves the macOS actual. See improvement file 2026-06/week-26/kmp-macos-pulltorefresh-expect-actual.md.
Detail-screen PTR coverage: ProjectDetailScreen, CompanyDetailScreen, DealDetailScreen (P30 batch 2) — all 3 now have pull-to-refresh via LumiRefreshable (stale-while-revalidate; error surfaced on failure, guard against double-refresh)✅ 2026-06-25isRefreshing: Boolean + refresh() added to each ScreenModel; LumiRefreshable wrapper added to each Screen composable; success/failure/guard tests added to each ScreenModel test. See improvement file docs/improvements/2026-06/week-26/kmp-detail-screen-pull-to-refresh.md
Mac routing: ContentView.contentForTab routes all 9 active sidebar tabs to KMPMacView — tasks, projects, chat, and workspace/hub moved from native SwiftUI to KMP; 5 unused @State vars removed; detail pane uses MacPlaceholderView for tasks/projects (KMP handles item detail inline, matching decisions/risks pattern)✅ 2026-06-24Mac KMP reuse coverage: 5/9 → 9/9 tabs. Native MacTasksViewModel/MacProjectsViewModel/ChatViewModel eliminated as active code paths. SharedScreenBridge.swift activation comment updated. See improvement file 2026-06/week-26/macos-kmp-tasks-projects-chat-hub.md
Mac activation: re-link shared.xcframework in LumiMac Frameworks build phase and verify SharedScreenBridge.swift #if canImport(shared) branch⏳Factory source parity and 25 sidebar routes do not establish a buildable Mac framework: macosArm64 is currently absent from the shared target graph. Restore/configure that target, build on a macOS host, re-link the framework, then verify the bridge and native-login parity.
Dead native file cleanup: delete migration-notice files that are no longer live code✅ 2026-07-04Deleted 6 tombstone Swift files: AccountScreens.swift, IntelligenceInboxScreens.swift, SubscriptionScreen.swift, AuthScreen.swift, GateScreens.swift, DetailScreens.swift. Removed dead WorkspaceSection + WorkspaceRow structs from MainTabView.swift (never instantiated after SharedLaneTabView KMP switch). Added DeadFileGuardTests.swift compile-time guard. Verified: project.pbxproj had 0 explicit references; SPM path: "Lumi" auto-discovery requires no manifest update. See improvement file docs/improvements/2026-07/week-27/ios-dead-native-file-cleanup-f78.md.
Platform parity script: scripts/check-platform-parity.sh enumerates all shared Screen objects and verifies each is registered in iOS shell, macOS shell, and Kotlin iOS entry point✅ 2026-07-02Script exists at scripts/check-platform-parity.sh and is wired into CI via .github/workflows/ci-kmp.yml step bash scripts/check-platform-parity.sh. Runs on deploy/* pushes that touch android/**, ios/Lumi/KMP/**, or ios/LumiMac/KMP/**.
Windows/Linux: desktopApp/src/main/kotlin/Main.kt Compose Desktop entry point consuming shared commonMain screen composables via jvmMain actuals⏳Compose Desktop app exists on main (android/desktopApp/). Main.kt uses application { Window { DesktopApp() } }; MainShell.kt mounts the 24 active shared screens, including PulseHomeScreen, BriefScreen, and TasksScreen, directly from com.locuno.lumi.shared.ui.*. The retired TodayScreen is no longer a desktop route; its card anatomy lives in Pulse needs-you-now. CI: .github/workflows/desktopApp-ci.yml runs on release/desktop branch. Production promotion (manual desktop-v* tag release) pending — Tauri-based production desktop not yet retired.

Decision boundary: F79 is the product target; F78 is one implementation strategy. The current repo still behaves as if F78 is intended, because shared KMP code is present and live. Therefore the current problem is best treated as implementation drift against an active architecture target, not as proof that F78 is obsolete. If product/engineering leadership decides the KMP strategy is no longer desired, this spec should be deprecated in one explicit change.

What exists and can be reused

AssetLocationReuse path
Shared KMP screensandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/{today,brief,pulse,lanes}Expand the same commonMain pattern to additional lists and detail flows
Shared KMP screen modelsandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/presentation/{today,brief,pulse}Reuse for future shared state ownership instead of adding more platform-local duplicates
iOS/macOS bridgeandroid/shared/src/iosMain/.../MainViewController.kt, ios/Lumi/KMP/SharedScreenBridge.swift, ios/LumiMac/KMP/SharedScreenBridge.swiftExtend factory coverage beyond lane roots into shared directory and breadth/detail flows; keep shell native
Android shell integrationandroid/app/src/main/kotlin/com/locuno/lumi/android/ui/LumiApp.kt, SharedKmpViewModel.ktReuse this injection path as shared coverage expands
Native lane and Workspace screensandroid/feature/**, ios/Lumi/UI/**Migrate in waves; do not duplicate them again in parallel

Migration phases (ordered by risk, lowest first)

  1. Phase 0–3 completed only for Today / Brief / Pulse: KMP scaffold, shared data, shared models, and shared UI are real.
  2. Wave 1 now materially executed: both mobile shells share all four F79 top-level roots and the shared Hub directory composition, while breadth/detail ownership remains mixed.
  3. Next wave — Hub breadth lists: migrate shared list/detail ownership for work, intelligence, business, and company directory surfaces before attempting edge features.
  4. Next wave — detail/action flows: approvals, decisions, commitments, risks, CRM, messages, teams, and settings should share state and screen composition where F30 demands parity.
  5. Parity gate last, not fictional: only add or enforce a platform-parity gate after the shared destination graph is broad enough to be meaningful.

Ownership inventory snapshot (2026-06-24, updated)

Surface familyShared KMPAndroid-nativeiOS-nativemacOS bridge
Today✅legacy native copy still present (deletion candidate)native copy still present (deletion candidate)✅
Brief✅legacy native copy still present (deletion candidate)native copy still present (deletion candidate)✅
Pulse form/readback✅legacy native copy still present (deletion candidate)native copy still present (deletion candidate)✅
F79 lane roots✅shared KMP mounted from Android shell✅🔲 (no sidebar route)
Tasks / Projects / Recurring / Horizon✅routed to KMP (native files deleted)routed to KMP (native files deleted)✅ Tasks+Projects via KMPMacView (PR #582); Recurring/Horizon no sidebar route
Approvals / Decisions / Commitments / Risks✅routed to KMP (native files deleted)routed to KMP (native files deleted, routing bugs fixed)✅ all four via KMPMacView — Decisions/Risks (PR #582), Commitments/Approvals (2026-06-24)
Messages / Hub / Teams✅routed to KMP (native files deleted)routed to KMP (native files deleted)✅ Hub via KMPMacView (PR #582 Workspace tab); Messages/Teams no sidebar route
CRM / Companies / Deals✅routed to KMP (native files deleted)routed to KMP (native files deleted)🔲 (no sidebar route)
Settings / account / org controls✅routed to KMP (native files deleted)routed to KMP (native files deleted)native Settings scene (HIG requirement)
Signal Inbox (F32)✅ 2026-06-23routed to KMP (no-op stub replaced)routed to KMP (native Swift deleted)✅ via KMPMacView (2026-06-24)
Follow-ups✅routed to KMProuted to KMP✅ via KMPMacView (2026-06-24)

See implementation audit for the evidence-backed repo inventory.


12. Open Questions / Decisions

These must be resolved (as ADR) before Phase 1 begins:

#QuestionOptionsRecommendation
1Navigation library in shared module(a) Compose Navigation (Google) · (b) Voyager · (c) Circuit (Cash App) · (d) DecomposeVoyager — simplest ScreenModel API, good iOS track record, actively maintained. Circuit is elegant but heavier. Decompose is powerful but complex.
2String resources in shared module(a) moko-resources · (b) Lyricist (compile-time from JSON) · (c) custom expect/actualmoko-resources — mature, well-documented for KMP; generates typed accessors from existing JSON locale files
3DI in shared module(a) Koin · (b) Kodein · (c) manual factoryKoin — multiplatform-first, zero-reflection on iOS (works with Kotlin/Native), used by most KMP reference projects
4Image loading in shared module(a) Coil 3 (CMP-ready) · (b) Kamel · (c) platform-side onlyCoil 3 — official CMP support as of 3.x, same API as Android
5Do top bars stay in Compose or move to platform shell on iOS?(a) Compose TopAppBar hidden, SwiftUI nav bar used · (b) Compose draws its own top bar on all platformsPlatform shell on iOS/macOS for Liquid Glass fidelity; Compose draws its own on Android. Controlled via expect/actual WindowChromeSpec.
6ADR for this architectural decisionRequired before implementationAuthor docs/ADR/0NN-compose-multiplatform-architecture.md covering: why KMP over Flutter/RN, why hybrid shell over pure-Compose, migration risk and rollback.

13. Bug / Change Log

2026-08-05 — Align the F78 architecture proof with F79’s four-tab V2 shell

Type: Contract and CI repair

Summary:

  • Replaced the stale F78 five-tab proof with exact assertions for F79’s canonical Lumi · Work · Business · Hub shell.
  • Updated the F78 CUE scope, authority boundary, requirement, and proof locator to the same four-tab contract.

Reason:

  • The shipped product contract had already moved to the four-tab V2 shell, but the F78 structural test and CUE declaration still encoded the retired five-tab wording and failed backend CI.

Impact:

  • F78 now guards the active F79 architecture instead of rejecting it. This changes no runtime navigation behavior and does not close any named-device proof gap.

Tests:

  • backend/tests/platform/test_f78_contract.py
  • Contract formatting, validation, generation, and graph tests.

2026-08-02 — Android deal-detail state no longer serializes transient CRM JSON

Type: Android navigation-state crash fix

Summary:

  • Sentry event 0ebe286474b947d486b8d902bdbb8526 showed DealDetailNavKey crashing in SavedStateEncoder while the Activity was saving state. The in-memory initialDeal DTO contains custom_fields: JsonObject, whose serializer is JSON-only.
  • initialDeal remains available for the immediate deal-detail render but is now @Transient, so persisted Navigation3 state contains only the stable dealId and reloads the DTO after process recreation.

Verification: :app:testDevDebugUnitTest --tests com.locuno.lumi.android.navigation.DealDetailNavKeyTest.

2026-07-12 — macOS Audit Log parity registration restored

Type: Shared-screen parity fix

Summary:

  • ui/auditlog already had an iOS factory but no matching macOS SharedScreen case or macosMain view-controller factory. The platform-parity gate therefore failed even though the shared screen exists.
  • The macOS bridge and factory now register the read-only, role-gated Audit Log surface. This is a registration repair only; it does not add a new navigation surface or change authorization.

Verification: bash scripts/check-platform-parity.sh.

2026-08-02 — LumiTab parity registration restored

Type: Shared-screen parity fix

Summary:

  • The new ui/lumitab directory was already hosted on iOS through the lumiHome compatibility route and had an iOS factory, but the parity gate derives the canonical name from the shared directory and therefore required an explicit lumitab registration on both native shells.
  • Added the iOS compatibility registration, the macOS SharedScreen.lumitab bridge route, and a macosMain LumiTabScreen host factory. This is source registration only; it does not add a sixth Mac sidebar tab or claim a buildable/linked macOS framework.

Verification: bash scripts/check-platform-parity.sh; python3 -m pytest -q backend/tests/platform/test_f78_contract.py.

2026-07-04 — Dead iOS tombstone files removed (closes F78 Implementation Audit gap)

Type: Shell cleanup — 6 tombstone Swift files deleted + 2 dead SwiftUI structs removed from MainTabView.swift.

Files deleted:

  • ios/Lumi/UI/Workspace/AccountScreens.swift — migration-notice-only (SwitchOrg, JoinOrg, CreateOrg, OrgDangerZone all routed to KMP OrgManagementScreen since 2026-07-02)
  • ios/Lumi/UI/Workspace/IntelligenceInboxScreens.swift — 10-line comment-only (F32 Signal Inbox migrated to KMP since 2026-06-23)
  • ios/Lumi/Billing/SubscriptionScreen.swift — migration-notice-only (migrated to KMP since 2026-06-25)
  • ios/Lumi/UI/Auth/AuthScreen.swift — migration-notice-only (all auth surfaces on KMP since ~2026-06-20)
  • ios/Lumi/UI/Auth/GateScreens.swift — migration-notice-only (JoinWithCodeView removed 2026-07-02; routing through KMP OrgManagementScreen)
  • ios/Lumi/UI/Features/DetailScreens.swift — StatusBadge struct with zero call sites outside the file

Dead structs removed from MainTabView.swift:

  • WorkspaceSection — never instantiated after WorkspaceTabView switched to SharedLaneTabView
  • WorkspaceRow — same

Safety verification:

  • grep across ios/Lumi.xcodeproj/project.pbxproj → 0 references to any deleted filename
  • ios/Package.swift uses path: "Lumi" SPM auto-discovery — no manifest update required
  • grep -r "StatusBadge\|WorkspaceSection\|WorkspaceRow" → 0 live call sites

New guard added: ios/UnitTests/Workspace/DeadFileGuardTests.swift — compile-level XCTest that verifies all org/subscription/signal-inbox workspace rows still resolve to non-nil actions after cleanup, confirming no live route was accidentally severed.

See improvement file docs/improvements/2026-07/week-27/ios-dead-native-file-cleanup-f78.md.


2026-07-02 — Status and §11 audit reconciled to converged shell-only reality

Type: Documentation correction — status line and Implementation Audit rows updated to reflect the actual repo state; no code changed.

What was stale:

  • Status line still said “partial hybrid rather than the intended shell-only split” — incorrect. The shell-only split is effectively achieved: 59 shared commonMain screens cover all feature families; Android and iOS native duplicate files have been deleted; Mac mounts all 25 sidebar tabs via KMPMacView; desktop has migrated to Compose Desktop (code on main).
  • scripts/check-platform-parity.sh was listed as a gap, but the script exists and is wired into CI (ci-kmp.yml step bash scripts/check-platform-parity.sh) — marked ✅.
  • Mac activation row cited “31 cases” — corrected to 32 (after makeAuthViewController 2026-07-02).
  • Windows/Linux row showed ☐ citing “blocked on macOS blocker 2” — blocker 2 resolved 2026-06-23; Compose Desktop app is on main and mounts shared screens. Updated to ⏳ (production promotion pending, not a code gap).

Genuinely pending gaps kept:

  1. Mac XCFramework re-link on a macOS host (env-blocked non-code step).
  2. Dead native file cleanup: AccountScreens.swift (retired SwiftUI structs) + IntelligenceInboxScreens.swift (10-line comment-only migration notice). Added as explicit ⏳ row.
  3. Desktop Compose Desktop production promotion (Tauri not yet retired from prod).
  4. One native LoginView in ios/LumiMac/Views/LoginView.swift — correctly remains until XCFramework re-link confirms parity.

Evidence basis:

  • Shared screen count: find android/shared/src/commonMain/kotlin -name "*Screen.kt" -path "*/ui/*" | wc -l → 59.
  • Parity script: ls scripts/check-platform-parity.sh → exists; grep "check-platform-parity" .github/workflows/ci-kmp.yml → line 36 bash scripts/check-platform-parity.sh.
  • Mac tabs: ContentView.swift header comment “All 25 shared Compose screens render via KMPMacView”; 28 KMPMacView( call sites.
  • Dead files: ls ios/Lumi/UI/Workspace/ → AccountScreens.swift (exists, “RETIRED” header), IntelligenceInboxScreens.swift (exists, 10-line comment only).
  • Desktop: MainShell.kt mounts the 24 active shared screens, including PulseHomeScreen, BriefScreen, and TasksScreen; the retired TodayScreen is absent. desktopApp-release.yml trigger is manual desktop-v* tag.

2026-07-02 — makeAuthViewController added to macosMain (closes factory parity gap)

Type: Build-correctness fix — makeAuthViewController was added to iosMain/MainViewController.kt by the F30 auth KMP migration (PR #899) but was not ported to macosMain. The Mac SharedScreenBridge.swift .auth case already dispatches to this symbol; its absence would have caused a link error once shared.xcframework is re-linked (the “Mac activation” step in §11).

macosMain/MainViewController.kt — 3 imports (AuthScreenModel, DefaultAuthRepository, AuthScreen) + makeAuthViewController(baseUrl, onAuthenticated) factory added in the Pre-auth section, mirroring the iosMain counterpart exactly. Uses ComposeNSViewController (AppKit) instead of ComposeUIViewController (UIKit); no new Kotlin content, no new Swift. macosMain ↔ iosMain factory parity: 32/32.

See improvement file: docs/improvements/2026-07/week-27/macos-kmp-auth-factory-parity.md


2026-06-28 — ApprovalsScreen now triggers .createApproval on iOS + Android (closes P30 wiring gap)

Type: UX wiring fix — closes the gap §11 noted at the 2026-06-25 P30 entry (line 684-686): CreateApprovalScreen + its iOS/Mac factory + .createApproval bridge case shipped on 2026-06-25, but the shared ApprovalsScreen had no create affordance to navigate to it, so the destination was unreachable on both platforms.

ApprovalsScreen.kt (commonMain) — @Composable signature gains optional onCreateApproval: (() -> Unit)? = null. When non-null, the title row renders a TextButton with Res.string.action_new (reuses the existing en+vi key shared with Decisions/Commitments). The previous LumiTrailingCount falls back to the else branch when no create callback is wired (e.g. on macOS until that lane gets its own create flow).

iOS — MainViewController.kt makeApprovalsViewController(…) now takes onCreateApproval: (() -> Unit)? as a required non-default parameter (KMP/Swift ObjC interop rule per check-kmp-ios-factory-no-default-lambdas). SharedScreenBridge.swift .approvals case forwards onCreated → onCreateApproval; MainTabView.swift already pushed .createApproval on that callback (mirroring .tasks → .createTask etc.) — the bridge wiring is the only iOS change required.

Android — LumiApp.kt entry<ApprovalsNavKey> now passes onCreateApproval = { navigator.navigate(CreateApprovalNavKey) }. The shadow-over-native entry registered on 2026-06-25 is unchanged.

macOS — untouched; the macosMain makeApprovalsViewController keeps its existing 3-arg signature (no create flow on Mac yet, parity deferred until Mac approvals workflow is scoped).

Regression coverage — Maestro .maestro/critical/21_create_approval_flow.yaml asserts Workspace → Approvals → ”+ New” → New approval page title → save → list reappears. Tests 16-20 added in the same PR cover Task Detail, Project Board, Company Detail, Create Company, and Hub Announcement detail navigation (independent regression guards for other previously-untested screens).

Verification: make check-kmp-ios-factory-no-default-lambdas → OK (0 violations). make ci-kmp + make ci-docs green on PR head. iOS Xcode build gated by ios-ci.yml on PR; Maestro suite runs out-of-band on seeded devices.


2026-06-27 — Complete LoadState four-state contract + stale-data preservation on Today screen

Type: UX-consistency fix — closes the merge-blocking gap in mobile-ux-patterns.md §1. Before this change, LoadState.Error carried no stale data (last was absent), so a failed pull-to-refresh would blank the screen instead of keeping prior content visible.

LoadState.kt — Error<T> promoted to generic with last: T? = null; added dataOrStale() extension and isFirstLoad property (both in commonMain, shared across all screens).

TodayScreenModel.kt —

  • TodayUiState defaults changed from Loading() to NotRequested (correct initial state per §1).
  • loadAll() captures stale data from the current Success state before launching the fetch.
  • toLoadState(last) now passes last to LoadState.Error so failures on refresh carry prior good data.
  • combinedTasks failure path also forwards staleTasks.

TodayScreen.kt — rendering updated to use dataOrStale() per section:

  • NotRequested / Loading(last=null) → skeleton (first load).
  • Loading(last=data) → stale rows visible while refresh is in flight.
  • Error(last=null) → blocking error row (no data ever loaded).
  • Error(last=data) → non-blocking error banner above stale rows.

Tests — three new tests in TodayScreenModelTest.kt:

  • initial state is NotRequested for all sections
  • failed refresh preserves stale data in Error_last
  • failed first load has null Error_last (no stale data)

Verification: compile-only (cloud CI cannot reach Google Maven for AGP 9.0.0); code reviewed against spec invariants.

See improvement file: docs/improvements/2026-06/week-04/loadstate-four-state-contract.md


2026-06-27 — P30 cleanup sweep: delete orphaned Android shell ViewModels and screen files

Type: Dead-code removal — android/feature/workspace/impl/ shell-only files made unreachable by P30 batch 2 (2026-06-25). WorkspaceEntry.kt had explicitly noted itself as “kept for reference until the next cleanup sweep”; this is that sweep.

Deleted (production, 17 files):

  • 7 ViewModels (ApprovalsViewModel, CrmViewModel, DecisionsViewModel, ProjectsViewModel, RecurringViewModel, SubscriptionViewModel, TasksViewModel) — all replaced by equivalent Voyager ScreenModels in android/shared/src/commonMain/.
  • 9 screen files (AnnouncementReaderSheet, CommitmentActionSheet, DecisionDetailSheet, FeatureStubScreen, LumiScreenColors, RiskActionSheet, SubscriptionScreen, TaskActionSheet, UpgradeGateContent) — all replaced by shared KMP composables.
  • navigation/WorkspaceEntry.kt — empty; Subscription migrated in a prior step, leaving only the package declaration and migration notice comment.

Deleted (tests, 9 files): seven app-level ViewModel tests (Approvals, Crm, Decisions, Projects, Recurring, Subscription, Tasks) and the Approvals and Decisions tests under feature/workspace/impl. Equivalent coverage lives in android/shared/src/commonTest/.

Edited (2 files): WorkspaceTests.kt removed three methods that directly referenced deleted ViewModels; ActionTests.kt removed obsolete approval/decision action classes. Remaining tests are unchanged.

Verification: compile-only (no emulator/simulator available in cloud CI); :feature:workspace:impl:compileDebugKotlin, :app:compileDevDebugKotlin, :shared:compileKotlinJvm, and :shared:jvmTest all green.

See improvement file: docs/improvements/2026-06/week-04/kmp-workspace-impl-dead-code-cleanup.md


2026-06-25 — Form-migration batch 2 (final): 6 edit/detail screens → shared KMP (P30)

Type: Platform Migration — native Android edit/detail screens → shared KMP commonMain (batch 2, completing P30). With this change all 13 P30 form/edit/detail screens are KMP; the form-screen migration is complete.

  • Screens migrated: EditTask, EditProject (load → prefill → PUT), ProjectDetail, RecurringDetail, CompanyDetail, DealDetail (load → render with inline actions). Each now lives once in commonMain and ships on Android, iOS, and the Mac shell from one source.
  • Per screen (commonMain):
    • presentation/<area>/<X>ScreenModel.kt — Voyager ScreenModel whose UiState holds a LoadState<entity>; init { load() } fetches via the repository (Loading→Success/Error idiom matching the list models). Edit models add form fields + isSaving/error and a submit(onSaved) that PUTs; detail models expose their inline action (runNow(onRan) for recurring, updateStatus(status) for the deal). testScope ctor param for tests.
    • presentation/<area>/<X>Repository.kt — focused interface + Default… impl wrapping the existing LumiApiClient load/update calls (reuses existing request DTOs).
    • ui/<area>/<X>Screen.kt — stateless @Composable; renders Loading/Error/Success of the LoadState; Lumi components + tokens only, no self-drawn top bar, every string via Res.string.*.
  • Per-id construction: unlike the singleton list/create models, edit/detail models are per-id — each Android entry<…NavKey> builds its model inside the entry from the NavKey id via a new id-parametrized factory on SharedKmpViewModel (mirroring taskDetailModel/dealDetailModel), remember(key.<id>)-scoped.
  • Inline actions preserved: ProjectDetail keeps its Edit affordance (→ EditProjectNavKey), RecurringDetail keeps Run-now (confirm dialog), DealDetail keeps its stage move (open/won/lost via updateDeal).
  • Deal-detail naming: the F50 activity-flow crm/DealDetailScreen is the canonical deal detail across shells. The old status-only implementation was removed; the workspace-api key and .dealStatusDetail bridge case remain only as compatibility aliases to that canonical screen. The canonical screen is wired to the workspace-api DealDetailNavKey as well as the app-module deal route.
  • iOS / Mac: makeEditTaskViewController(…, taskId, onSaved), makeEditProjectViewController, makeProjectDetailViewController(…, onEdit), makeRecurringDetailViewController(…, onRan), makeCompanyDetailViewController, and the compatibility alias makeDealStatusDetailViewController added to both iosMain and macosMain MainViewController. Both SharedScreenBridge.swift files gain matching id-carrying enum cases + dispatch (edit screens reuse onCreated as the saved sink; ProjectDetail reuses onRowSelected for its Edit affordance). Full 6/6 symbol parity.
  • i18n: 37 new edit/detail keys added to both values/strings.xml and values-vi/strings.xml (existing recurring_frequency_*/recurring_impact_*/crm_deal_status_* keys reused, not duplicated).
  • Cleanup: the 6 native screens/{EditTask,EditProject,ProjectDetail,RecurringDetail, CompanyDetail,DealDetail}Screen.kt and the now-orphaned screens/DateField.kt (used only by the deleted EditProjectScreen) were deleted; WorkspaceEntry.kt now registers only the Subscription entry. Zero dangling refs.
  • Verification: :shared:compileKotlinJvm, :app:compileDevDebugKotlin, :feature:workspace:impl:compileDebugKotlin all BUILD SUCCESSFUL; 24 new commonTest cases pass via :shared:jvmTest. iOS/Mac build in CI (no local Xcode).
  • As-built: P30’s batches 2 and 3 were delivered in one PR because the shared wiring files (LumiApp.kt, SharedKmpViewModel.kt, both SharedScreenBridge.swift, WorkspaceEntry.kt) are touched by all six screens and cannot be split.

2026-06-25 — Form-migration batch 1: 6 create-only forms → shared KMP (P30)

Type: Platform Migration — native Android forms → shared KMP commonMain (batch 1, replicating the CreateApproval reference).

  • Screens migrated: CreateTask, CreateProject, CreateRecurring, CreateDecision, CreateCompany, CreateDeal. Each now lives once in commonMain and ships on Android, iOS, and the Mac shell from one source.
  • Per screen (commonMain):
    • presentation/<area>/Create<X>ScreenModel.kt — Voyager ScreenModel holding all form-field state + isSaving/error flags; submit(onCreated) validates required fields, trims, maps blanks→null, runCatching the repo call, fires onCreated on success. testScope ctor param.
    • presentation/<area>/Create<X>Repository.kt — focused interface + Default… impl wrapping the existing LumiApiClient create call (reuses existing request DTOs).
    • ui/<area>/Create<X>Screen.kt — stateless @Composable(model, onCreated); Lumi components + tokens only, no self-drawn top bar, every string via Res.string.*.
  • Deviations from the reference (KMP constraints): Material3 DatePicker/ExposedDropdownMenu are Android-only and unavailable in commonMain, so date inputs render as YYYY-MM-DD text fields (like the reference’s money field), single-select dropdowns (priority/status/frequency/ impact/deal-status) render as LumiFilterChip rows, and company customer/supplier/partner tags render as toggle LumiFilterChips. CreateDecision keeps its dynamic options list — the model owns options: List<String> with add/edit-by-index/remove (min one row); blank rows are dropped on submit, matching the native ViewModel.
  • i18n: all field/label/error/chip keys added to both composeResources/values/strings.xml and values-vi/strings.xml (tasks_*, projects_*, recurring_*, decisions_*, crm_*). tasks_create_title/projects_create_title reused from the existing list namespace.
  • Tests: 6 commonTest Create<X>ScreenModelTest classes (42 cases total: validation, trim/ null-mapping, success callback, error flag, plus dynamic-options add/edit/remove for Decision and dual-required-field validation for Recurring/Deal). All green via :shared:jvmTest.
  • iOS + Mac: makeCreate<X>ViewController(…, onCreated) in both iosMain and macosMain MainViewController.kt; .create<X> cases (+ screenName titles) in both ios/Lumi/.../SharedScreenBridge.swift and ios/LumiMac/.../SharedScreenBridge.swift.
  • Android: entry<Create<X>NavKey> for all 6 added to LumiApp.kt (before workspaceEntry, shadowing native; imports kept ktlint-alphabetical) + create<X>Model in SharedKmpViewModel. Deleted the 6 native feature/workspace/impl/.../screens/Create*.kt files and their entry<…NavKey> blocks/imports in WorkspaceEntry.kt. The native internal fun DateField (previously defined in CreateProjectScreen.kt, used by the still-native EditProjectScreen) was extracted into a new screens/DateField.kt so the edit flow keeps compiling. The native list ViewModels (TasksViewModel, ProjectsViewModel, RecurringViewModel, DecisionsViewModel, CrmViewModel) were retained — they back the still-native edit/detail screens.
  • Drive-by fix: ui/crm/DealsScreen.kt had a pre-existing @Composable-out-of-scope error (a stringResource call inside a LazyColumn forEach but above the item {} block) that broke :shared:compileKotlinJvm on origin/main; moved the call into the item scope so the jvm target (and thus the new tests) compiles.
  • Verification: :shared:compileKotlinJvm BUILD SUCCESSFUL; :shared:jvmTest --tests "*Create*ScreenModelTest" 49/49 pass (42 new + 7 reference); :app:compileDevDebugKotlin and :feature:workspace:impl:compileDebugKotlin BUILD SUCCESSFUL. iOS/Mac builds run in CI (no local Xcode); bridges static-checked for symbol parity.

2026-06-25 — CreateApproval ported to shared KMP (form-migration reference, P30)

Type: Platform Migration — native Android form → shared KMP commonMain (reference implementation).

Summary:

  • New shared (commonMain):
    • presentation/approvals/CreateApprovalScreenModel.kt — Voyager ScreenModel holding all form-field state (title/description/category/moneyAmount + titleError/isSaving/ submitError); submit(onCreated) validates (blank title → error), trims, maps blanks to null, calls the repo, fires onCreated on success. APPROVAL_CATEGORIES constant.
    • presentation/approvals/CreateApprovalRepository.kt — focused interface + Default… impl wrapping the existing api.createApproval(ApprovalCreateRequest(…)) (kept separate from ApprovalsRepository, which is list/approve/reject only).
    • ui/approvals/CreateApprovalScreen.kt — stateless @Composable(model, onCreated); Lumi components only (LumiPageTitle, LumiFormField, LumiMultilineField, LumiFilterChip category picker, LumiPrimaryButton, LumiStatusBanner); all strings via Res.string.*; no self-drawn top bar (shell owns chrome, per §6).
  • i18n: 18 approvals_* form keys added to both values/strings.xml and values-vi/strings.xml (title/saving/failed/title-required/description/category/amount/save + 6 category labels).
  • Tests: commonTest/.../presentation/approvals/CreateApprovalScreenModelTest.kt — 7 cases (initial state, field updaters clear error, blank-title validation no-op, trim+null mapping + onCreated, non-blank forwarding, failure sets submitError, edit-after-failure clears error). All green via ./gradlew :shared:jvmTest.
  • iOS: iosMain/MainViewController.kt makeCreateApprovalViewController(…, onCreated); .createApproval case (+ onCreated closure) in ios/Lumi/KMP/SharedScreenBridge.swift. No iPhone route triggers it yet (the shared ApprovalsScreen list has no create entry point — matching the prior Android state where CreateApprovalNavKey was registered but never navigated to); the factory + bridge case are ready for whenever a create affordance is added.
  • Mac: macosMain/MainViewController.kt makeCreateApprovalViewController(…, onCreated) (keeps macosMain↔iosMain factory parity); .createApproval case (+ onCreated) in ios/LumiMac/KMP/SharedScreenBridge.swift.
  • Android: entry<CreateApprovalNavKey> added to LumiApp.kt (before workspaceEntry, so it shadows the native entry; onCreated = goBack()); createApprovalModel added to SharedKmpViewModel. Deleted native feature/workspace/impl/.../screens/CreateApprovalScreen.kt + its entry<CreateApprovalNavKey> block in WorkspaceEntry.kt. Native ApprovalsViewModel retained — still exercised by existing unit tests and not referenced by any other live screen; flagged in P30 as a future deletion candidate, not removed here to avoid deleting real test coverage.
  • Drive-by fix: added the missing import kotlin.test.assertNull to the pre-existing SettingsScreenModelTest.kt (dropped in commit bbf5e5dc1’s ktlint reorder) — without it the whole :shared test module failed to compile, blocking every shared unit test.

Reason:

  • F78’s promise is “every screen written once.” List/detail surfaces are shared, but the 13 create/edit form screens were still native-only and dual-maintained. CreateApproval is the reference the other 12 copy. Plan + sequencing: P30.

Impact:

  • One more screen runs from a single commonMain source; one native Android file deleted.
  • Establishes the repeatable form-migration pattern (model + repo + screen + i18n + test + iOS/Mac factory + bridge + Android wiring + native deletion).

Tests:

  • ./gradlew :shared:compileKotlinMetadata → BUILD SUCCESSFUL (commonMain compiles for all targets).
  • ./gradlew :shared:jvmTest --tests "*CreateApprovalScreenModelTest" → 7/7 pass.
  • Full :shared:jvmTest surfaces 22 pre-existing failures in the “reload preserves stale data in Loading” pattern across many untouched models (incl. the reference OrgManagementScreenModelTest); a runTest eager-dispatch test-design flake, not introduced here (on pristine origin/main the test module did not even compile, so these never previously ran). None of the new CreateApproval tests are affected.
  • iOS/Mac/Xcode builds not run locally (no Xcode); bridges static-checked for symbol parity. CI gates.

See: P30 implementation plan

2026-06-23 — WorkspaceLaneScreen i18n + Lumi design tokens (Rule 3 + Rule 4 compliance)

Type: Compliance fix — i18n (Rule 3) + design tokens (Rule 4).

Summary:

  • WorkspaceLaneScreen.kt (commonMain) — 26 hardcoded English string literals replaced with Res.string.* via stringResource(). Routing keys (passed to onRowSelected) preserved unchanged. Rendering switched from LaneScaffold to direct LazyColumn + LumiPageTitle + LumiSectionHeader + LumiDirectoryRow.
  • LaneScaffold.kt (commonMain) — deleted; was the only remaining file in lanes/ using stock Card and raw MaterialTheme.colorScheme.onSurfaceVariant.
  • values/strings.xml and values-vi/strings.xml — 14 new workspace_* keys added (EN + VI).
  • LaneManifestTest.kt — workspace sections contain expected row counts per group test added.

Reason:

  • WorkspaceLaneScreen was the last F79 lane screen not using Res.string.*, giving Vietnamese-locale users English labels on the widest-breadth screen in the app (entry to all 21 surfaces). LaneScaffold was also the sole remaining file in lanes/ using non-Lumi design tokens.

Impact:

  • Workspace tab displays localized labels in all supported locales, Vietnamese first.
  • LaneScaffold.kt removed — no stock Compose tokens remain in lanes/.
  • All four F79 top-level roots now comply with Rule 3 + Rule 4; Pulse remains a nested shared surface.

See: docs/improvements/2026-06/week-26/kmp-workspace-lane-i18n-design-tokens.md

2026-06-25 — macOS Blocker 1 (final): makeNotificationsViewController added to macosMain (32/32 parity)

Type: Platform Gap Fix — missing factory function completing macosMain ↔ iosMain parity.

Summary:

  • android/shared/src/macosMain/kotlin/…/MainViewController.kt was missing makeNotificationsViewController (F77). The initial 2026-06-23 batch wrote 31 factories; F77 was added to iosMain after that and not mirrored.
  • SharedScreenBridge.swift already had .notifications → makeNotificationsViewController(…) in its #if canImport(shared) branch — the call site was ready, the symbol was absent.
  • Fix: added 3 import lines + 9-line factory body mirroring the iosMain pattern. Factory count: macosMain = iosMain = 32.
  • Both compile blockers (1 + 2) are now fully resolved. ./gradlew :shared:linkReleaseMacosArm64 unblocked. XCFramework rebuild + Xcode re-link on a macOS host activates all 31 SharedScreen cases.

See: docs/improvements/2026-06/week-04/macos-kmp-notifications-factory-parity.md

2026-06-23 — macOS Blocker 2: PullToRefreshBox expect/actual (unblocks macOS XCFramework build)

Type: Platform Compatibility Fix — expect/actual to isolate iOS-only M3 API from macOS/JVM targets.

Summary:

  • LumiMobilePrimitives.kt (commonMain) — LumiRefreshable refactored: PullToRefreshBox removed from commonMain; public function now delegates to internal expect fun lumiRefreshableImpl; ExperimentalMaterial3Api and PullToRefreshBox imports removed.
  • TodayScreen.kt (commonMain) — replaced direct PullToRefreshBox call + @OptIn(ExperimentalMaterial3Api::class) with LumiRefreshable; removed both problematic imports.
  • iosMain/LumiRefreshable.kt — new actual providing real PullToRefreshBox for iOS (iosArm64 + iosSimulatorArm64).
  • macosMain/LumiRefreshable.kt — new actual no-op (pull-to-refresh gesture is meaningless on macOS trackpad/mouse).
  • jvmMain/LumiRefreshable.kt — new actual no-op (JVM/desktop).

Reason:

  • PullToRefreshBox from androidx.compose.material3 is an Android/iOS CMP API with no macOS Kotlin/Native binding. Any commonMain import caused unresolved reference at ./gradlew :shared:linkReleaseMacosArm64 and ./gradlew :shared:compileKotlinJvm, blocking the macOS XCFramework build and the Windows/Linux desktop build.

Impact:

  • ./gradlew :shared:linkReleaseMacosArm64 unblocked — macOS XCFramework can now be built.
  • ./gradlew :shared:compileKotlinJvm unblocked — desktop (Windows/Linux) shared KMP build unblocked.
  • iOS pull-to-refresh unchanged (real PullToRefreshBox still used via iosMain actual).
  • macOS and JVM show a plain scrollable view; user can trigger a refresh via the inline retry action or by navigating away and back.

See: docs/improvements/2026-06/week-26/kmp-macos-pulltorefresh-expect-actual.md

2026-06-24 — Mac: all 9 sidebar tabs routed to KMPMacView

Type: Mac Shell — contentForTab routing completion.

Summary:

  • ios/LumiMac/Views/ContentView.swift — Tasks, Projects, Chat, and Workspace tabs moved from native SwiftUI views (TasksView, ProjectsView, ChatView, MacPlaceholderView) to KMPMacView. Five unused @State vars removed. Detail pane for tasks/projects uses MacPlaceholderView (consistent with decisions/risks). .workspace → .hub KMP screen.
  • ios/LumiMac/KMP/SharedScreenBridge.swift — activation comment updated (blockers resolved; only Xcode Framework-link step remains).

Impact:

  • Mac KMP reuse coverage: 5/9 → 9/9 active tabs.
  • Native Mac-only ViewModels (MacTasksViewModel, MacProjectsViewModel, ChatViewModel) eliminated as active code paths.
  • No new native Swift content code added; the four screens run from commonMain once xcframework is linked in Xcode.

See: docs/improvements/2026-06/week-26/macos-kmp-tasks-projects-chat-hub.md

2026-06-23 — F32 Signal Inbox migrated to shared KMP

Type: Platform Migration — native iOS → shared KMP commonMain.

Summary:

  • SignalInboxRepository.kt, SignalInboxScreenModel.kt, SignalInboxScreen.kt created in commonMain.
  • SignalInboxScreenModelTest.kt (18 tests) created in commonTest.
  • LumiApiClient.kt — 5 Signal Inbox API methods added; LumiDtos.kt — 2 DTOs added.
  • Android: SignalInboxNavKey.kt added; SharedKmpViewModel wired; LumiApp.kt no-op stub replaced.
  • iOS: makeSignalInboxViewController added to iosMain; .signalInbox case added to SharedScreenBridge.swift; MainTabView.swift routes to KMP; IntelligenceInboxScreens.swift native implementation deleted.
  • 29 signal_inbox_* i18n keys added to both EN and VI string resources.

Reason:

  • Android had a silent no-op; iOS had a parallel native implementation. Consolidating into shared KMP eliminates the divergence, adds test coverage, and increases shared-code coverage metric.

2026-06-22 — Rule 3 fix: CRM/Messages/Hub screens now locale-aware

Type: Rule 3 fix (merge-blocking)

Summary:

  • CompaniesScreen.kt — replaced 9 hardcoded English strings with stringResource(Res.string.crm_companies_*); Res + stringResource imports added.
  • DealsScreen.kt — replaced 7 hardcoded English strings with stringResource(Res.string.crm_deals_*); deal status label (won/lost/open) now mapped via when + stringResource inside DealCard composable instead of raw replaceFirstChar.
  • MessagesScreen.kt — replaced 6 hardcoded English strings with stringResource(Res.string.messages_*).
  • HubScreen.kt — replaced 9 hardcoded English strings with stringResource(Res.string.hub_*); Pinned/Acknowledged/Acknowledge labels localized inside AnnouncementCard.
  • values/strings.xml — added 30 new keys: 8 crm_companies_*, 9 crm_deals_*, 5 messages_*, 8 hub_*.
  • values-vi/strings.xml — matching Vietnamese translations for all 30 new keys.

Reason:

  • All four screens were routed live on both Android and iOS in the wave23 PR (2026-06-22) but still contained hardcoded English strings — Rule 3 violation. Vietnamese-locale CEOs saw English text on these live screens.

Impact:

  • Single commonMain change: both Android and iOS corrected simultaneously.
  • No behavior change in English locale; Vietnamese locale now displays correct strings.
  • Zero platform-shell changes required.

Tests:

  • Type-safe: Res.string.* references are compile-time-checked — missing keys cause build errors.
  • Cloud environment: AGP 9.0.0 unavailable in Gradle cache — compile/test could not be run locally. CI gates the PR. Pattern identical to prior localized screens (TasksScreen, ApprovalsScreen) which passed CI.

See: docs/improvements/2026-06/week-04/f78-rule3-crm-messages-hub-i18n.md

2026-06-22 — Settings KMP migration: Rule 3 violation fixed, native shells deleted

Type: Rule 3 fix + shared-code coverage increase

Summary:

  • android/shared/.../ui/settings/SettingsScreen.kt — replaced all hardcoded English strings with stringResource(Res.string.settings_*) (13 keys); removed dead onLeaveOrg callback that was always {} in both shells
  • android/shared/composeResources/values/strings.xml — added 13 settings_* keys (en)
  • android/shared/composeResources/values-vi/strings.xml — added 13 settings_* keys (vi)
  • android/shared/src/iosMain/.../MainViewController.kt — removed onLeaveOrg param from factory
  • ios/Lumi/KMP/SharedScreenBridge.swift — updated .settings case to not pass onLeaveOrg
  • ios/Lumi/UI/MainTabView.swift — route .settings to KMPComposeView, add DI environment
  • android/app/.../ui/LumiApp.kt — inline entry<SettingsNavKey> using shared KMP screen; fix “Manage organization” routing bug (was SettingsNavKey, now SwitchOrgNavKey)
  • android/MainActivityViewModel.kt — add fun signOut() public method
  • Deleted: android/feature/settings/impl/.../SettingsScreen.kt (486 lines)
  • Deleted: android/feature/settings/impl/.../navigation/SettingsEntry.kt (13 lines)
  • Deleted: ios/Lumi/UI/Settings/SettingsScreen.swift (219 lines)
  • Net: ~700 lines deleted, 0 behavior regressions, Rule 3 resolved

2026-06-22 — Wave 2+3 routing: 12 list screens now served from shared KMP on both platforms

Type: Feature (routing wire-up + dead-code removal)

Summary:

  • android/app/.../ui/LumiApp.kt — added 12 explicit entry<NavKey> blocks for Wave 2+3 shared screens (Tasks, Projects, Recurring, Horizon, Approvals, Decisions, Commitments, Risks, Messages, Hub, Teams, CRM/Companies) before the workspaceEntry call so Navigation3’s first-match-wins routes to KMP. Mirrors the pattern of Wave 1’s FollowUps/Contacts/Forecast/HubDocs entries.
  • android/feature/workspace/impl/.../navigation/WorkspaceEntry.kt — removed the 12 now-shadowed list-level entries; kept all create/edit/detail flows and intentionally-native screens (Notifications, Subscription, SwitchOrg).
  • Deleted 12 native list screen .kt files + 4 ViewModels (CommitmentsViewModel, HorizonViewModel, RisksViewModel, TeamsViewModel) whose entire purpose was duplicating shared KMP state management.
  • ios/Lumi/UI/MainTabView.swift — added 3 missing enum cases (.decisions, .commitments, .risks) to SharedLaneRoute; replaced 10 native screen renders with KMPComposeView; added 3 new KMP cases; fixed 3 routing bugs (Decisions was .brief, Commitments was .today, Risks was .today — all now correct).
  • ios/Lumi/UI/Features/DetailScreens.swift — removed 8 detail/edit screen structs + 2 badge types (PriorityBadge, SeverityBadge) with zero remaining references; kept only StatusBadge (still referenced by IntelligenceInboxScreens.swift).
  • Deleted 3 native iOS files: ChatScreen.swift (dead), WorkspaceScreens.swift (11 structs all replaced by KMP), CrmDealScreens.swift (DealListScreen + DealDetailScreen replaced by KMP).

Reason:

  • Wave 2+3 shared KMP screens existed in commonMain since 2026-06-19 but both shells still routed to native duplicates. The shared code was dead-weight; native duplicates were the live product. This wires up what was already built.

Impact:

  • 12 screens on Android and iOS now run from a single commonMain source.
  • 16 Android files + 3 iOS files deleted (~2,000+ lines of dual-maintenance eliminated).
  • 3 iOS routing bugs fixed (CEO tapping Decisions, Commitments, Risks no longer lands on wrong screen).
  • Ownership snapshot updated from “2026-06-18” to “2026-06-22” to reflect the new routing state.

Tests:

  • Cloud environment: AGP 9.0.0 unavailable in Gradle cache — compile/test could not be run locally. CI gates the PR. Pattern is identical to Wave 1 entries (FollowUpsNavKey → FollowUpsScreen) which passed CI on prior PRs.
  • No emulator/device available in cloud host — stated honestly per AGENTS-mobile.md §3.

See: docs/improvements/2026-06/week-04/f78-wave23-shared-kmp-routing.md

2026-06-18 — Repoint shell assumptions to F79

Type: Documentation Change

Summary:

  • Replaced early-phase shell wording that implied an old generic five-tab model with explicit references to F79.
  • Clarified that future shared navigation must implement Pulse / Work Graph / Business / Company Hub / Workspace, with supporting modes nested inside those lanes.

Reason:

  • Compose Multiplatform architecture should not freeze an already-superseded navigation model into the shared shell.

Impact:

  • Future F78 implementation work now inherits the correct mobile IA target.

Tests:

  • None. Spec-only change.

2026-06-18 — Correct F78 status to partial hybrid

Type: Documentation Change

Summary:

  • Replaced the stale “complete” status with an evidence-backed in-progress state.
  • Corrected the Implementation Audit and Current Behavior sections to reflect the actual repo: shared KMP exists for Today, Brief, and Pulse, while most mobile breadth still lives in native Android and SwiftUI code.
  • Added an ownership snapshot and linked implementation audit.

Reason:

  • The previous wording overstated the migration. The repo does not yet satisfy F78’s own promise that screen-level UI content is shared broadly and shells stay thin.

Impact:

  • Future mobile work inherits the real migration target instead of a false “already done” claim.

Tests:

  • None. Documentation-only correction.

2026-06-18 — Add Wave 1 execution handoff

Type: Documentation Change

Summary:

  • Added a focused Wave 1 implementation plan for the first shared-ownership expansion after the F79 shell migration: shared lane roots, shared Workspace directory, and cleanup of duplicate native Today/Brief/Pulse launch surfaces.
  • Linked the plan from the implementation index and from the broader F30 mobile plan.

Reason:

  • The repo had a solid diagnosis of the KMP drift but no narrowly scoped execution lane for the first corrective wave.

Impact:

  • Future work can start from a bounded, test-first migration slice instead of reopening the audit from scratch.

Tests:

  • None. Planning-only change.

2026-06-17 — iOS & macOS shell wiring (Phase 4 bridge committed)

Type: Feature

Summary:

  • android/shared/src/iosMain/kotlin/.../MainViewController.kt — Kotlin entry points makeTodayViewController, makeBriefViewController, makePulseViewController exposed to Swift; each returns a ComposeUIViewController backed by the shared ScreenModel + repository.
  • ios/Lumi/KMP/SharedScreenBridge.swift — KMPComposeView: UIViewControllerRepresentable wrapping the KMP entry points; stub fallback (KMPNotLinkedView) shown until shared.xcframework is linked in Xcode → target → Frameworks.
  • ios/Lumi/UI/MainTabView.swift — legacy note: Today/Brief/Pulse-era shell assumptions existed in early F78 planning. Future implementation must follow F79 instead: Pulse / Work Graph / Business / Company Hub / Workspace, with supporting modes nested inside those lanes.
  • ios/LumiMac/KMP/SharedScreenBridge.swift — macOS equivalent using NSViewControllerRepresentable; same stub-then-activate pattern.
  • ios/LumiMac/Views/ContentView.swift — Today, Brief, Pulse, Decisions, Risks routed to KMPMacView; Tasks, Projects, Chat still native at this point.

Remaining step (after 2026-06-24 routing PR):

  1. Build XCFramework: cd android && GRADLE_USER_HOME=/tmp/gradle-home ./gradlew :shared:assembleXCFramework
  2. Drag android/shared/build/XCFrameworks/release/shared.xcframework → Xcode → Lumi + LumiMac targets → Frameworks, Libraries, and Embedded Content

Verified: compileKotlinIosSimulatorArm64 ✅ · compileKotlinMacosArm64 ✅ · xcodebuild build-for-testing ✅

2026-06-17 — All phases shipped (Phases 0–5)

Type: Feature

Summary:

  • Phase 0: KMP scaffold — shared module compiles for JVM + iosArm64 + iosSimulatorArm64 + macosArm64; appleMain source set; Ktor client w/ Darwin engine.
  • Phase 1: Data layer — Ktor LumiApiClient with all domain DTOs, SecureStore expect/actual, LumiApiClientTest (7 tests).
  • Phase 2: ViewModel layer — Voyager ScreenModel + Koin DI added; BriefScreenModel (6 tests), TodayScreenModel (9 tests), PulseScreenModel (5 tests); shared LoadState<T> sealed interface; kotlin.time.Clock for date handling (kotlinx-datetime 0.8.0 removed Clock.System); CI job shared-kmp added.
  • Phase 3: Compose Multiplatform UI — CMP 1.10.3 added (jetbrainsCompose plugin + compose.runtime/foundation/material3); TodayScreen, BriefScreen, PulseScreen in commonMain/ui/; zero platform imports; all screens consume ScreenModel StateFlow.
  • Phase 4: iOS framework — fixed SecureStore.apple.kt (NSMutableDictionary subscript broken in Kotlin/Native → NSUserDefaults placeholder, TODO migrate to Keychain); :shared:linkDebugFrameworkIosSimulatorArm64 → BUILD SUCCESSFUL.
  • Phase 5: macOS framework — :shared:linkDebugFrameworkMacosArm64 → BUILD SUCCESSFUL.

Tests: 28 jvmTests passing (LumiApiClient 7 · Brief 6 · Today 9 · Pulse 5 · Platform 1), 0 failures.

Notes:

  • The native platform shells (SwiftUI iOS ComposeUIViewController bridge, Android ComposeView) remain to be wired to the shared screens — this is the Xcode/Android Studio step that requires the app targets.
  • SecureStore.apple.kt uses NSUserDefaults as a placeholder; must migrate to Keychain before shipping sensitive tokens to production.

2026-06-17 — Spec created

Type: Feature

Summary:

  • Initial draft of F78 — Compose Multiplatform migration spec.

Reason:

  • Current dual-codebase approach (Android Compose + iOS SwiftUI) cannot scale to 77+ features without tripling the mobile team. KMP+CMP allows sharing 80–90% of code while retaining native chrome quality on each platform.

Impact:

  • F30 and F47 architecture sections are superseded by F78 §4 when this spec is agreed.
  • No user-visible behavior changes — this is an implementation architecture change.
  • Requires ADR before Phase 1 implementation begins.

Tests:

  • None yet — spec only.

Notes:

  • Do not start Phase 1 before F70/F71 wedge is validated (per strategy v2 “validation before velocity” doctrine). This spec defines the architecture; implementation is gated on wedge proof.