Files
recipe/.planning/phases/01-project-infrastructure-module-wiring/01-04-PLAN.md
2026-04-29 20:54:01 +02:00

28 KiB

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: "<application android:name=".MainApplication" ...>" 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication  // top-level
interface KoinApplication
typealias KoinAppDeclaration = KoinApplication.() -> Unit

From io.insert-koin:koin-dsl:

fun module(createdAtStart: Boolean = false, moduleDeclaration: ModuleDeclaration): Module

From io.insert-koin:koin-android (androidMain only):

// package org.koin.android.ext.koin
fun KoinApplication.androidContext(context: Context): KoinApplication

From co.touchlab:kermit:

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):

@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):

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):

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):

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@android:style/Theme.Material.Light.NoActionBar">

Current iOS Swift entry (to replace):

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:

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:

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:

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:

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 <FileName>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 <acceptance_criteria> - 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) </acceptance_criteria> 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:

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 <application> element. Do NOT modify any other attribute or element.

Resulting <application> tag:

<application
        android:name=".MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Material.Light.NoActionBar">

The <activity> child element (with android:name=".MainActivity") stays unchanged. The full XML structure (declarations, <manifest>, <intent-filter>) 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 <acceptance_criteria> - 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 <application> element - composeApp/src/androidMain/AndroidManifest.xml still contains android:name=".MainActivity" on the <activity> element (unchanged) - composeApp/src/androidMain/AndroidManifest.xml still contains <intent-filter> with MAIN action + LAUNCHER category (unchanged) - composeApp/src/androidMain/AndroidManifest.xml top-level <manifest> declaration unchanged </acceptance_criteria> 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:

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:

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<X>() 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):

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 <acceptance_criteria>
    • 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) </acceptance_criteria> All four platform entry points call configureLogging() then initKoin() before composition; iOS Swift wires KoinIosKt.doInitKoin() exactly once in init().

<threat_model>

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.
</threat_model>
Phase-level verification for this plan:
  • Task 1, 2, 3 <automated> 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 <automated> blocks — Plan 05 + Plan 07 run the compile gates. Keep this plan's verification grep-fast (<5s total).

<success_criteria>

  • 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) </success_criteria>
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.