From cc5002d1dfcf8e0fd1ace6622694ba63f6bb2524 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 24 Apr 2026 19:41:05 +0200 Subject: [PATCH 1/4] feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - initKoin() helper with optional KoinAppDeclaration config - empty appModule placeholder (Phase 2+ extends) - configureLogging() sets Kermit tag 'recipe' (D-15) - iOS doInitKoin() bridge → Swift symbol KoinIosKt.doInitKoin --- .../commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt | 8 ++++++++ .../src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt | 10 ++++++++++ .../kotlin/dev/ulfrx/recipe/logging/Logging.kt | 8 ++++++++ .../src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt | 8 ++++++++ 4 files changed, 34 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt create mode 100644 composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt new file mode 100644 index 0000000..f0461a6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt @@ -0,0 +1,8 @@ +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 +} diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt new file mode 100644 index 0000000..1ce7e87 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt @@ -0,0 +1,10 @@ +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) +} diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt new file mode 100644 index 0000000..8fbd6a6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt @@ -0,0 +1,8 @@ +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. +} diff --git a/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt b/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt new file mode 100644 index 0000000..becaab7 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt @@ -0,0 +1,8 @@ +package dev.ulfrx.recipe.di + +import dev.ulfrx.recipe.logging.configureLogging + +fun doInitKoin() { + configureLogging() + initKoin() +} From 8cd608a981bd4938180b174c0839bafa90d04718 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 24 Apr 2026 19:41:22 +0200 Subject: [PATCH 2/4] feat(01-04): add Android MainApplication + manifest registration - MainApplication.onCreate calls configureLogging() then initKoin { androidContext(...) } - AndroidManifest registers android:name=".MainApplication" --- composeApp/src/androidMain/AndroidManifest.xml | 1 + .../kotlin/dev/ulfrx/recipe/MainApplication.kt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index cdba621..2ba19e1 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,7 @@ Date: Fri, 24 Apr 2026 19:41:51 +0200 Subject: [PATCH 3/4] feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit - JVM main: configureLogging() + initKoin() before application { Window } - Wasm main: configureLogging() + initKoin() before ComposeViewport (PITFALL #8) - iOSApp.swift: import ComposeApp + init { KoinIosKt.doInitKoin() } (PITFALL #4) --- .../jvmMain/kotlin/dev/ulfrx/recipe/main.kt | 20 ++++++++++++------- .../webMain/kotlin/dev/ulfrx/recipe/main.kt | 6 +++++- iosApp/iosApp/iOSApp.swift | 5 +++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt b/composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt index d7717c9..9467cf1 100644 --- a/composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt +++ b/composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt @@ -2,12 +2,18 @@ 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() = application { - Window( - onCloseRequest = ::exitApplication, - title = "recipe", - ) { - App() +fun main() { + configureLogging() + initKoin() + application { + Window( + onCloseRequest = ::exitApplication, + title = "recipe", + ) { + App() + } } -} \ No newline at end of file +} diff --git a/composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt b/composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt index 8490d3d..be8e22c 100644 --- a/composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt +++ b/composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt @@ -2,10 +2,14 @@ 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() } -} \ No newline at end of file +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 927e0b9..500c459 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -1,7 +1,12 @@ import SwiftUI +import ComposeApp @main struct iOSApp: App { + init() { + KoinIosKt.doInitKoin() + } + var body: some Scene { WindowGroup { ContentView() From eaa88fff367ffe0e3c583a8f6a7569c89e472897 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 24 Apr 2026 19:44:47 +0200 Subject: [PATCH 4/4] docs(01-04): add SUMMARY for Koin + Kermit bootstrap plan --- .../01-04-SUMMARY.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md b/.planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md new file mode 100644 index 0000000..0e74f69 --- /dev/null +++ b/.planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md @@ -0,0 +1,138 @@ +--- +phase: 01-project-infrastructure-module-wiring +plan: 04 +subsystem: client-bootstrap +tags: [koin, kermit, di, logging, ios-bridge, android-application, wasm-bootstrap] +requires: + - 01-02 (build-logic conventions providing Koin + Kermit dependencies via recipe.kotlin.multiplatform) + - 01-03 (composeApp/build.gradle.kts wired to convention plugin + libs.koin.android in androidMain) +provides: + - "initKoin(config: KoinAppDeclaration?): KoinApplication — single bootstrap helper" + - "appModule: Koin Module — empty placeholder; Phase 2+ extends with authModule, syncModule, catalogModule" + - "configureLogging() — sets Kermit Logger.setTag(\"recipe\")" + - "KoinIosKt.doInitKoin() — Swift-callable iOS bridge" + - "MainApplication: Android Application subclass invoking configureLogging + initKoin on process boot" +affects: + - "All future phases (2-11) plug Koin modules into appModule and call Logger.x { } via Kermit" + - "Phase 2 (Auth) will register authModule; Phase 4 (SyncEngine) will register syncModule singleton" +tech-stack: + added: [] + patterns: + - "Single initKoin() call site per platform entry point (PITFALL #4 — no double-init on iOS)" + - "configureLogging() ALWAYS precedes initKoin() so Koin module loading can use Kermit" + - "App.kt (@Composable) NEVER calls startKoin (Pattern 4 anti-pattern guard)" + - "iOS Kotlin bridge: top-level fun doInitKoin in KoinIos.kt → Swift symbol KoinIosKt.doInitKoin" + - "Wasm init order: configureLogging → initKoin → ComposeViewport (PITFALL #8)" +key-files: + created: + - 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 + modified: + - 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 + unchanged_by_design: + - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt # anti-pattern guard: no startKoin in @Composable + - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt # PITFALL #4: Koin started exclusively in iOSApp.init() + - iosApp/iosApp/ContentView.swift # already wraps MainViewControllerKt.MainViewController() +decisions: + - "Kermit tag = \"recipe\" (D-15) — exact string" + - "appModule is empty in Phase 1 (D-14); Phase 2+ adds modules" + - "Single iOS Koin call site is iOSApp.init() (PITFALL #4 mitigation)" + - "androidContext(this@MainApplication) — qualified `this` because initKoin lambda receiver is KoinApplication" +metrics: + tasks_completed: 3 + tasks_total: 3 + files_created: 5 + files_modified: 4 + duration: ~10m + completed: 2026-04-24 +--- + +# Phase 1 Plan 4: Koin + Kermit Bootstrap Wiring — Summary + +Wired the Koin DI container and Kermit structured logger across all four composeApp platform entry points (Android Application subclass, iOS SwiftUI App.init, JVM desktop main, Wasm browser main) with a single `initKoin()` helper in commonMain and an empty `appModule` placeholder that Phase 2+ extends. + +## What was built + +### Task 1 — commonMain DI + logging + iOS bridge (commit `cc5002d`) + +Created four files: + +- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`** — exports `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { config?.invoke(this); modules(appModule) }`. The optional `config` lambda is how Android passes `androidContext(...)` and how Phase 2+ tests can inject overrides without touching the helper itself. +- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`** — declares `val appModule = module { }` (empty per D-14). Phase 2 adds `authModule`, Phase 4 adds `syncModule`, etc. +- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`** — `fun configureLogging() { Logger.setTag("recipe") }`. Kermit's per-platform writers (OSLog/LogCat/println) install themselves by default; setting the tag is the only required call. +- **`composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`** — `fun doInitKoin() { configureLogging(); initKoin() }`. The top-level `fun` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` automatically (Kotlin/Native generates `Kt` for top-level decls). + +### Task 2 — Android MainApplication + manifest (commit `8cd608a`) + +- **`composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`** — `class MainApplication : Application()` whose `onCreate()` calls `super.onCreate()`, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }`. Qualified `this@MainApplication` is required because the `initKoin { }` lambda receiver is `KoinApplication`, not the `Application`. +- **`composeApp/src/androidMain/AndroidManifest.xml`** — added `android:name=".MainApplication"` as the first attribute on ``. All other attributes and the ``/`` subtree preserved verbatim. + +### Task 3 — JVM + Wasm + Swift entry points (commit `fd3e7e1`) + +- **`composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`** — converted `fun main() = application { ... }` (single-expression) into a body block: `configureLogging()` → `initKoin()` → `application { Window(title = "recipe") { App() } }`. Window title and exit handler preserved. +- **`composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`** — same init order before `ComposeViewport { App() }`. `@OptIn(ExperimentalComposeUiApi::class)` retained. Defensive against PITFALL #8 (Wasm composition running before DI is ready) — Phase 1 has no ViewModels so the symptom would not surface yet, but the shape is correct from day 1. +- **`iosApp/iosApp/iOSApp.swift`** — added `import ComposeApp` (matches framework basename set by `recipe.kotlin.multiplatform`) and `init() { KoinIosKt.doInitKoin() }`. The `WindowGroup { ContentView() }` body is unchanged. `MainViewController.kt` and `ContentView.swift` were intentionally NOT modified — Koin is bootstrapped exclusively from `iOSApp.init()` (PITFALL #4 mitigation). + +## Init order invariant (every platform) + +``` +configureLogging() → installs Kermit tag "recipe" +initKoin() → starts Koin with empty appModule +[platform composition entry — application { } / ComposeViewport { } / ComposeUIViewController { } / setContent { }] +``` + +## Deviations from Plan + +None — plan executed exactly as written. All 3 tasks completed; all artifacts produced; all `` satisfied. + +## Confirmations (per `` section of PLAN) + +- Kermit tag = `"recipe"` (D-15) — set in `configureLogging()`. +- `appModule` content: empty (D-14) — `val appModule = module { }`. +- `App.kt` NOT modified (anti-pattern guard). +- `MainViewController.kt` NOT modified (PITFALL #4 guard — Koin started outside). +- `ContentView.swift` NOT modified (already wraps `MainViewControllerKt.MainViewController()`). + +## Threat Mitigations Verified + +| Threat ID | Mitigation in delivered code | +|-----------|------------------------------| +| T-01-04-01 (Koin double-init iOS) | `KoinIosKt.doInitKoin()` is the only init call site on iOS; `MainViewController.kt` does not call `startKoin`. | +| T-01-04-02 (Wasm init order) | webMain `main()` orders `configureLogging() → initKoin() → ComposeViewport { }`. | +| T-01-04-03 (App.kt calling startKoin) | `App.kt` unchanged; verified no `startKoin` reference outside `Koin.kt`. | + +## Verification gates + +- All three task `` grep blocks passed. +- No build files modified → `tools/verify-no-version-literals.sh` and `tools/verify-shared-pure.sh` remain at exit 0. +- Compile gates (`./gradlew build`, `:composeApp:jvmTest`) deferred to Plan 07 per the verification block in 01-04-PLAN.md. + +## Commits + +- `cc5002d` — feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge +- `8cd608a` — feat(01-04): add Android MainApplication + manifest registration +- `fd3e7e1` — feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit + +## Self-Check: PASSED + +Files verified to exist on disk: +- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt +- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt +- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt +- FOUND: composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt +- FOUND: composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt +- FOUND: composeApp/src/androidMain/AndroidManifest.xml (modified, contains `android:name=".MainApplication"`) +- FOUND: composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (modified) +- FOUND: composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (modified) +- FOUND: iosApp/iosApp/iOSApp.swift (modified) + +Commits verified in `git log`: +- FOUND: cc5002d +- FOUND: 8cd608a +- FOUND: fd3e7e1