Files
recipe/.planning/phases/01-project-infrastructure-module-wiring/01-04-PLAN.md
2026-04-24 16:21:25 +02:00

496 lines
28 KiB
Markdown

---
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 <application>"
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\\("
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- These come from the Koin library (already wired via recipe.kotlin.multiplatform in Plan 02) -->
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
<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):
```swift
import SwiftUI
@main
struct iOSApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create commonMain DI + logging files and iOS Kotlin bridge</name>
<files>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</files>
<read_first>
- .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")
</read_first>
<action>
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 `<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.
</action>
<verify>
<automated>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</automated>
</verify>
<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>
<done>Koin + Kermit commonMain wiring is in place; iOS bridge exposes a Swift-callable `KoinIosKt.doInitKoin()`.</done>
</task>
<task type="auto">
<name>Task 2: Create MainApplication.kt + register in AndroidManifest.xml</name>
<files>composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt, composeApp/src/androidMain/AndroidManifest.xml</files>
<read_first>
- 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)
</read_first>
<action>
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 `<application>` element. Do NOT modify any other attribute or element.
Resulting `<application>` tag:
```xml
<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.
</action>
<verify>
<automated>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</automated>
</verify>
<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>
<done>Android Application subclass starts Koin + Kermit on process boot; manifest registers the subclass.</done>
</task>
<task type="auto">
<name>Task 3: Wire JVM + Wasm main() entries and Swift iOSApp.swift</name>
<files>composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt, iosApp/iosApp/iOSApp.swift</files>
<read_first>
- 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)
</read_first>
<action>
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<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):
```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.
</action>
<verify>
<automated>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</automated>
</verify>
<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>
<done>All four platform entry points call `configureLogging()` then `initKoin()` before composition; iOS Swift wires `KoinIosKt.doInitKoin()` exactly once in `init()`.</done>
</task>
</tasks>
<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>
<verification>
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).
</verification>
<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>
<output>
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.
</output>