--- phase: 01-project-infrastructure-module-wiring plan: 04 type: execute wave: 2 depends_on: [01, 02] files_modified: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt - composeApp/src/androidMain/AndroidManifest.xml - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt - iosApp/iosApp/iOSApp.swift autonomous: true requirements: [INFRA-02] requirements_addressed: [INFRA-02] must_haves: truths: - "initKoin() is defined once in commonMain and called exactly once per platform entry point (no double-init — PITFALL #4)" - "configureLogging() runs BEFORE initKoin() on every platform (so Koin module loading can use Kermit)" - "App.kt (@Composable) never calls startKoin — Koin is started outside composition (anti-pattern guard in Pattern 4)" - "appModule is an empty Koin module placeholder; Phase 2+ adds authModule, syncModule, etc." - "Kermit tag is 'recipe' (D-15)" - "iOS Swift side calls KoinIosKt.doInitKoin() inside iOSApp.init() — one call site" - "Android uses MainApplication registered via android:name=\".MainApplication\" in AndroidManifest.xml" - "wasmJs main() initializes Koin + logging BEFORE ComposeViewport { App() } (PITFALL #8 future-proof)" artifacts: - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt" provides: "initKoin(config: KoinAppDeclaration? = null): KoinApplication helper invoking startKoin { modules(appModule) }" exports: ["initKoin"] - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt" provides: "Empty val appModule = module { } placeholder" exports: ["appModule"] - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt" provides: "configureLogging() — Logger.setTag(\"recipe\")" exports: ["configureLogging"] - path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt" provides: "fun doInitKoin() { configureLogging(); initKoin() } — exported as Swift symbol KoinIosKt.doInitKoin" exports: ["doInitKoin"] - path: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt" provides: "class MainApplication : Application() { onCreate → configureLogging(); initKoin { androidContext(this) } }" - path: "composeApp/src/androidMain/AndroidManifest.xml" provides: "" contains: "android:name=\".MainApplication\"" - path: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt" provides: "Desktop main() invoking configureLogging() + initKoin() before application { Window { App() } }" - path: "composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt" provides: "Wasm main() invoking configureLogging() + initKoin() before ComposeViewport { App() } (PITFALL #8)" - path: "iosApp/iosApp/iOSApp.swift" provides: "Swift @main struct with init() { KoinIosKt.doInitKoin() } and import ComposeApp" contains: "import ComposeApp", "KoinIosKt.doInitKoin()" key_links: - from: "iosApp/iosApp/iOSApp.swift" to: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt" via: "Kotlin top-level fun doInitKoin → Swift symbol KoinIosKt.doInitKoin()" pattern: "KoinIosKt\\.doInitKoin\\(\\)" - from: "composeApp/src/androidMain/AndroidManifest.xml" to: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt" via: "android:name=\".MainApplication\" attribute on " pattern: "android:name=\"\\.MainApplication\"" - from: "MainApplication.onCreate / iOSApp.init / jvm main / wasm main" to: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt" via: "initKoin() call" pattern: "initKoin\\(" --- Wire the Koin + Kermit bootstrap across every composeApp platform entry point. Create the two commonMain source files (`di/Koin.kt`, `di/AppModule.kt`, `logging/Logging.kt`), the iOS Kotlin bridge (`iosMain/di/KoinIos.kt`), the Android `Application` subclass + manifest registration, modify the JVM + Wasm entry points to call `configureLogging() → initKoin()` before composition, and modify Swift's `iOSApp.swift` to call `KoinIosKt.doInitKoin()` inside `init()`. The Kermit tag is `"recipe"` (D-15); the Koin module is an empty placeholder (D-14) that Phase 2+ extends. Purpose: Phase 1 proves the DI + logging wiring is correct from day 1 so Phase 2 (Auth) can add `authModule`, Phase 4 can add `syncModule`, etc. without revisiting the bootstrap mechanics. PITFALL #4 (double-init on iOS) is neutralized by concentrating all startup into one `initKoin()` helper with a single call site per platform. Output: 9 files created or modified (6 new Kotlin files, 1 manifest edit, 2 existing entry-point rewrites, 1 Swift rewrite). No ViewModels yet — Phase 1 has no screens beyond the template. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md @.planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md @.planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md @.planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md @composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt @composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt @composeApp/src/androidMain/AndroidManifest.xml @composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt @composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt @composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt @iosApp/iosApp/iOSApp.swift @CLAUDE.md From io.insert-koin:koin-core: ```kotlin fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication // top-level interface KoinApplication typealias KoinAppDeclaration = KoinApplication.() -> Unit ``` From io.insert-koin:koin-dsl: ```kotlin fun module(createdAtStart: Boolean = false, moduleDeclaration: ModuleDeclaration): Module ``` From io.insert-koin:koin-android (androidMain only): ```kotlin // package org.koin.android.ext.koin fun KoinApplication.androidContext(context: Context): KoinApplication ``` From co.touchlab:kermit: ```kotlin object Logger { fun setTag(tag: String) // plus .i { }, .d { }, .e { }, .w { } methods on Logger companion } ``` From existing composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt (do NOT modify): ```kotlin @Composable @Preview fun App() { /* template body — stays as-is */ } ``` From existing composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt (do NOT modify — sibling reference): ```kotlin package dev.ulfrx.recipe import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent // class MainActivity : ComponentActivity() { ... setContent { App() } } ``` From existing composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt (do NOT modify): ```kotlin package dev.ulfrx.recipe import androidx.compose.ui.window.ComposeUIViewController fun MainViewController() = ComposeUIViewController { App() } ``` Current Android manifest shape (attributes to preserve when adding android:name): ```xml ``` Current iOS Swift entry (to replace): ```swift import SwiftUI @main struct iOSApp: App { var body: some Scene { WindowGroup { ContentView() } } } ``` Task 1: Create commonMain DI + logging files and iOS Kotlin bridge composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt, composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 840-870 (Koin bootstrap canonical excerpts: initKoin + appModule + doInitKoin) - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 933-948 (Kermit bootstrap: Logger.setTag + init order) - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 675-690 (PITFALL #4 — single call site per platform, never from inside @Composable) - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 710-796 (pattern assignments for all 4 files) - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-14 (Koin empty appModule), D-15 (Kermit tag "recipe") Create 4 new files. **File 1: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`**: ```kotlin package dev.ulfrx.recipe.di import org.koin.core.KoinApplication import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { config?.invoke(this) modules(appModule) } ``` **File 2: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`**: ```kotlin package dev.ulfrx.recipe.di import org.koin.dsl.module // Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc. val appModule = module { // intentionally empty in Phase 1 } ``` **File 3: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`**: ```kotlin package dev.ulfrx.recipe.logging import co.touchlab.kermit.Logger fun configureLogging() { Logger.setTag("recipe") // Platform log writers (OSLog iOS, LogCat Android, System.out JVM/Wasm) install by default. } ``` **File 4: `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`**: ```kotlin package dev.ulfrx.recipe.di import dev.ulfrx.recipe.logging.configureLogging fun doInitKoin() { configureLogging() initKoin() } ``` CRITICAL notes (PITFALL #4 / #10): - The top-level `fun doInitKoin()` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` (Kotlin generates `Kt` for top-level declarations). - `doInitKoin()` is the SINGLE iOS entry point. `MainViewController()` (the `ComposeUIViewController` factory) must NOT call `startKoin` or `initKoin` — it assumes Koin is already started. - `configureLogging()` runs BEFORE `initKoin()` so Koin module loading can use Kermit. Do NOT add any expect/actual declarations — the iOS bridge is a plain top-level function, and Kermit's multiplatform Logger handles the platform-specific writer selection internally. test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt && test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt && test -f composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'package dev.ulfrx.recipe.di' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'fun initKoin(config: KoinAppDeclaration? = null)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'startKoin' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'modules(appModule)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'val appModule = module' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt && grep -q 'Logger.setTag("recipe")' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt && grep -q 'fun doInitKoin' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'configureLogging()' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'initKoin()' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt` exists and contains package declaration `package dev.ulfrx.recipe.di` - `Koin.kt` imports `org.koin.core.KoinApplication`, `org.koin.core.context.startKoin`, `org.koin.dsl.KoinAppDeclaration` - `Koin.kt` defines exactly one top-level function `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication` whose body is `startKoin { config?.invoke(this); modules(appModule) }` - `AppModule.kt` exists and contains package declaration `package dev.ulfrx.recipe.di` - `AppModule.kt` imports `org.koin.dsl.module` - `AppModule.kt` declares `val appModule = module { }` (empty — D-14) - `Logging.kt` exists and contains package declaration `package dev.ulfrx.recipe.logging` - `Logging.kt` imports `co.touchlab.kermit.Logger` - `Logging.kt` defines `fun configureLogging()` whose body calls `Logger.setTag("recipe")` (D-15 — exact string) - `KoinIos.kt` exists and contains package declaration `package dev.ulfrx.recipe.di` - `KoinIos.kt` imports `dev.ulfrx.recipe.logging.configureLogging` - `KoinIos.kt` defines `fun doInitKoin()` whose body is `configureLogging(); initKoin()` in that exact order - No file references `startKoin` directly outside `Koin.kt` (grep `startKoin` across composeApp/src returns only Koin.kt) - `App.kt` is NOT modified (anti-pattern guard — startKoin never called from inside @Composable) Koin + Kermit commonMain wiring is in place; iOS bridge exposes a Swift-callable `KoinIosKt.doInitKoin()`. Task 2: Create MainApplication.kt + register in AndroidManifest.xml composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt, composeApp/src/androidMain/AndroidManifest.xml - composeApp/src/androidMain/AndroidManifest.xml (current 22-line content — target of edit) - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt (sibling reference for androidMain package + imports) - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 895-911 (canonical MainApplication.kt) - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 800-849 (MainApplication + manifest deltas) - composeApp/build.gradle.kts (verify `libs.koin.android` was added to androidMain.dependencies in Plan 03) Create one new file and edit one existing file. **Create: `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`**: ```kotlin package dev.ulfrx.recipe import android.app.Application import dev.ulfrx.recipe.di.initKoin import dev.ulfrx.recipe.logging.configureLogging import org.koin.android.ext.koin.androidContext class MainApplication : Application() { override fun onCreate() { super.onCreate() configureLogging() initKoin { androidContext(this@MainApplication) } } } ``` CRITICAL: - `package dev.ulfrx.recipe` (not `dev.ulfrx.recipe.android` — matches the existing `MainActivity.kt` sibling). - `androidContext(this@MainApplication)` — the qualified `this` is required because the `initKoin { ... }` lambda's `this` is a `KoinApplication`, not the Application. - `configureLogging()` runs FIRST, then `initKoin { ... }` — establishes the required order (PATTERNS.md "Init order on every platform entry"). - `org.koin.android.ext.koin.androidContext` comes from `io.insert-koin:koin-android` (catalog alias `libs.koin.android`, added to `composeApp/build.gradle.kts` androidMain deps in Plan 03). **Edit: `composeApp/src/androidMain/AndroidManifest.xml`** — add `android:name=".MainApplication"` as the first attribute on the `` element. Do NOT modify any other attribute or element. Resulting `` tag: ```xml ``` The `` child element (with `android:name=".MainActivity"`) stays unchanged. The full XML structure (declarations, ``, ``) is preserved — only the single `android:name=".MainApplication"` attribute is added. test -f composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q '^package dev.ulfrx.recipe$' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'class MainApplication : Application()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'override fun onCreate()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'configureLogging()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'androidContext(this@MainApplication)' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'android:name="\.MainApplication"' composeApp/src/androidMain/AndroidManifest.xml && grep -q 'android:name="\.MainActivity"' composeApp/src/androidMain/AndroidManifest.xml - `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt` exists - Package declaration is exactly `package dev.ulfrx.recipe` (matches sibling `MainActivity.kt`) - Imports include `android.app.Application`, `dev.ulfrx.recipe.di.initKoin`, `dev.ulfrx.recipe.logging.configureLogging`, `org.koin.android.ext.koin.androidContext` - Class declaration is `class MainApplication : Application()` - `onCreate()` body calls `super.onCreate()` first, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }` — in exactly that order - `composeApp/src/androidMain/AndroidManifest.xml` contains literal `android:name=".MainApplication"` attribute on the `` element - `composeApp/src/androidMain/AndroidManifest.xml` still contains `android:name=".MainActivity"` on the `` element (unchanged) - `composeApp/src/androidMain/AndroidManifest.xml` still contains `` with MAIN action + LAUNCHER category (unchanged) - `composeApp/src/androidMain/AndroidManifest.xml` top-level `` declaration unchanged Android Application subclass starts Koin + Kermit on process boot; manifest registers the subclass. Task 3: Wire JVM + Wasm main() entries and Swift iOSApp.swift composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt, iosApp/iosApp/iOSApp.swift - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (current content — target of rewrite) - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (current content — target of rewrite) - iosApp/iosApp/iOSApp.swift (current 11-line content — target of rewrite) - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 874-931 (Swift + Desktop + Wasm bootstrap) - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 733-747 (PITFALL #8 — Wasm init order) - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 852-937 (per-file deltas for these three files) Replace three file contents. **Replace: `composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`**: ```kotlin package dev.ulfrx.recipe import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import dev.ulfrx.recipe.di.initKoin import dev.ulfrx.recipe.logging.configureLogging fun main() { configureLogging() initKoin() application { Window( onCloseRequest = ::exitApplication, title = "recipe", ) { App() } } } ``` **Replace: `composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`**: ```kotlin package dev.ulfrx.recipe import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import dev.ulfrx.recipe.di.initKoin import dev.ulfrx.recipe.logging.configureLogging @OptIn(ExperimentalComposeUiApi::class) fun main() { configureLogging() initKoin() ComposeViewport { App() } } ``` CRITICAL (PITFALL #8): `configureLogging()` and `initKoin()` MUST run BEFORE `ComposeViewport { }` — otherwise the first `koinViewModel()` inside composition throws. Phase 1 has no ViewModels, so this is defensive — but the shape must be correct from day 1. **Replace: `iosApp/iosApp/iOSApp.swift`** (Swift file, not Kotlin): ```swift import SwiftUI import ComposeApp @main struct iOSApp: App { init() { KoinIosKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView() } } } ``` CRITICAL: - `import ComposeApp` — matches the framework basename set in `recipe.kotlin.multiplatform` (D-20 / PITFALL #10). The existing file does NOT import ComposeApp; add it. - `init() { KoinIosKt.doInitKoin() }` — the Swift symbol `KoinIosKt` is auto-generated from Kotlin file `KoinIos.kt` in package `dev.ulfrx.recipe.di` (created in Task 1). - `ContentView()` invocation stays unchanged; `ContentView.swift` already calls `MainViewControllerKt.MainViewController()` which returns a `ComposeUIViewController` — do NOT modify `ContentView.swift`. - Do NOT call `startKoin` from `MainViewController()` — iOS init is centralized in `iOSApp.init()` to avoid PITFALL #4. grep -q '^package dev.ulfrx.recipe$' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'configureLogging()' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'initKoin()' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'Window(' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q '^package dev.ulfrx.recipe$' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'configureLogging()' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'initKoin()' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'ComposeViewport' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'import ComposeApp' iosApp/iosApp/iOSApp.swift && grep -q 'KoinIosKt.doInitKoin()' iosApp/iosApp/iOSApp.swift && grep -q 'init() {' iosApp/iosApp/iOSApp.swift - `composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt` has `configureLogging()` on a line preceding `initKoin()`, and both precede `application {` (init order invariant) - JVM main imports include `dev.ulfrx.recipe.di.initKoin` AND `dev.ulfrx.recipe.logging.configureLogging` - JVM main preserves `Window(onCloseRequest = ::exitApplication, title = "recipe") { App() }` - `composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt` has `configureLogging()` on a line preceding `initKoin()`, and both precede `ComposeViewport {` (PITFALL #8) - Web main imports include `dev.ulfrx.recipe.di.initKoin` AND `dev.ulfrx.recipe.logging.configureLogging` - Web main still has `@OptIn(ExperimentalComposeUiApi::class)` on `fun main()` - `iosApp/iosApp/iOSApp.swift` contains exactly `import SwiftUI` AND `import ComposeApp` (both imports required) - `iosApp/iosApp/iOSApp.swift` contains `init() {` followed by `KoinIosKt.doInitKoin()` — exactly one call - `iosApp/iosApp/iOSApp.swift` preserves `@main struct iOSApp: App { ... body: some Scene { WindowGroup { ContentView() } } }` - `MainViewController.kt` is NOT modified (the existing file returns `ComposeUIViewController { App() }` — Koin bootstrapped outside, PITFALL #4) - `App.kt` is NOT modified (anti-pattern guard) All four platform entry points call `configureLogging()` then `initKoin()` before composition; iOS Swift wires `KoinIosKt.doInitKoin()` exactly once in `init()`. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Platform process start → DI container initialization | Each platform (Android onCreate, iOS App.init, JVM main, Wasm main) is a trusted bootstrap context; `initKoin()` is called once, from code we control. | | Kotlin top-level fun → Swift generated symbol | `KoinIos.kt` in package `dev.ulfrx.recipe.di` is compiled into the `ComposeApp.framework` Swift binary as `KoinIosKt.doInitKoin()`. No runtime risk — compile-time symbol mapping. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-01-04-01 | Denial of Service | Koin double-init on iOS second cold launch (PITFALL #4) | mitigate | Only `iOSApp.init()` calls `KoinIosKt.doInitKoin()`. `MainViewController.kt` does NOT call `startKoin`. Task 3 acceptance criteria explicitly prohibits `startKoin` in `MainViewController.kt`. If Koin is accidentally started twice, `KoinApplicationAlreadyStartedException` fires on launch — visible and easy to diagnose. | | T-01-04-02 | Denial of Service | Wasm composition runs before Koin init (PITFALL #8) | mitigate | Task 3 explicitly orders `configureLogging() → initKoin() → ComposeViewport { }`. Phase 1 has no ViewModels so the symptom would not surface until Phase 5+, but the order is correct from day 1. | | T-01-04-03 | Tampering | `App.kt` calling `startKoin` from inside @Composable | mitigate | Task 1 + Task 3 acceptance criteria prohibit modification of `App.kt`. `App.kt` template preserves the anti-pattern-free shape. | | T-01-04-04 | Information Disclosure | Kermit logs leaking sensitive data | accept | Phase 1 has no sensitive data in the codebase (no auth, no user records, no PII). Kermit tag `"recipe"` is a build identifier, not a secret. Revisit when Phase 2 (Auth) introduces tokens — at that point, Kermit's `.i { }` lambda evaluation prevents accidental string concat of secrets if authors follow the lambda idiom. | | T-01-04-05 | Elevation of Privilege | Android manifest `android:name=".MainApplication"` registers custom Application subclass | accept | This is the standard Android lifecycle — `MainApplication.onCreate()` runs in the app's own process, same privilege as `MainActivity`. No escalation. | Phase-level verification for this plan: - Task 1, 2, 3 `` blocks pass (grep-based). - `tools/verify-no-version-literals.sh` continues to exit 0 (no build files modified). - `tools/verify-shared-pure.sh` continues to exit 0 (shared/ not touched). - Plan 07 runs `./gradlew build` and `./gradlew :composeApp:jvmTest` — those will exercise `initKoin()` via composition and catch any Koin config error. No `./gradlew` invocation is in this plan's `` blocks — Plan 05 + Plan 07 run the compile gates. Keep this plan's verification grep-fast (<5s total). - 6 new commonMain/iosMain/androidMain Kotlin files created (Koin.kt, AppModule.kt, Logging.kt, KoinIos.kt, MainApplication.kt — and the init order is correct in each) - AndroidManifest.xml has `android:name=".MainApplication"` attribute added - JVM + Wasm main() entries call `configureLogging()` THEN `initKoin()` BEFORE composition - `iOSApp.swift` imports `ComposeApp` and calls `KoinIosKt.doInitKoin()` in `init()` - `App.kt` unmodified (anti-pattern guard) - `MainViewController.kt` unmodified (PITFALL #4 guard) After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md` recording: 6 files created + 3 files modified paths, Kermit tag set to `"recipe"`, Koin appModule content (empty), and confirmation that `App.kt` / `MainViewController.kt` / `ContentView.swift` were NOT modified.