diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f27757e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.{kt,kts}]
+# ktlint configuration for Compose Multiplatform.
+# - function-naming is disabled because @Composable functions and Kotlin/Native
+# entry-point factories (e.g. MainViewController) are PascalCase by convention.
+# - filename is disabled because Compose-Multiplatform entry-point files
+# (jvmMain/main.kt, webMain/main.kt) follow the Kotlin `fun main()` convention.
+ktlint_standard_function-naming = disabled
+ktlint_standard_filename = disabled
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md
index 7956bc7..d3c48c7 100644
--- a/.planning/REQUIREMENTS.md
+++ b/.planning/REQUIREMENTS.md
@@ -96,12 +96,12 @@
### Infrastructure & build
-- [ ] **INFRA-01**: Gradle version catalog at `gradle/libs.versions.toml` is the single source of truth for library versions
-- [ ] **INFRA-02**: `build-logic/` convention plugins centralize Kotlin/Compose/test configuration across modules
-- [ ] **INFRA-03**: iOS Kotlin/Native binary options set from day 1: `kotlin.native.binary.objcDisposeOnMain=false`, `gc=cms`
+- [x] **INFRA-01**: Gradle version catalog at `gradle/libs.versions.toml` is the single source of truth for library versions
+- [x] **INFRA-02**: `build-logic/` convention plugins centralize Kotlin/Compose/test configuration across modules
+- [x] **INFRA-03**: iOS Kotlin/Native binary options set from day 1: `kotlin.native.binary.objcDisposeOnMain=false`, `gc=cms`
- [ ] **INFRA-04**: Server Docker image builds and deploys to user's homelab alongside Authentik
- [ ] **INFRA-05**: Flyway migrations run automatically on server startup in a known order
-- [ ] **INFRA-06**: `shared/commonMain` contains only domain models + API DTOs — no UI, no HTTP, no DB code
+- [x] **INFRA-06**: `shared/commonMain` contains only domain models + API DTOs — no UI, no HTTP, no DB code
- [ ] **INFRA-07**: App is distributed to partner via TestFlight (iOS) for initial dogfooding
## v2 Requirements
@@ -224,12 +224,12 @@ Populated during roadmap creation. Each v1 requirement maps to exactly one phase
| UI-07 | Phase 10: UI Chrome & Haze Liquid-Glass Polish | Pending |
| UI-08 | Phase 5: Recipe Catalog (Read Path) | Pending |
| UI-09 | Phase 10: UI Chrome & Haze Liquid-Glass Polish | Pending |
-| INFRA-01 | Phase 1: Project Infrastructure & Module Wiring | Pending |
-| INFRA-02 | Phase 1: Project Infrastructure & Module Wiring | Pending |
-| INFRA-03 | Phase 1: Project Infrastructure & Module Wiring | Pending |
+| INFRA-01 | Phase 1: Project Infrastructure & Module Wiring | Complete |
+| INFRA-02 | Phase 1: Project Infrastructure & Module Wiring | Complete |
+| INFRA-03 | Phase 1: Project Infrastructure & Module Wiring | Complete |
| INFRA-04 | Phase 11: Localization & iOS Deployment | Pending |
| INFRA-05 | Phase 3: Households, Membership & Server Data Foundation | Pending |
-| INFRA-06 | Phase 1: Project Infrastructure & Module Wiring | Pending |
+| INFRA-06 | Phase 1: Project Infrastructure & Module Wiring | Complete |
| INFRA-07 | Phase 11: Localization & iOS Deployment | Pending |
**Coverage:**
diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 47af9ed..891cb9f 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -8,7 +8,7 @@
## Phases
-- [ ] **Phase 1: Project Infrastructure & Module Wiring** — Running-but-empty KMP client + Ktor server with all build infra baked in
+- [x] **Phase 1: Project Infrastructure & Module Wiring** — Running-but-empty KMP client + Ktor server with all build infra baked in
- [ ] **Phase 2: Authentication Foundation** — User signs in through Authentik (OIDC+PKCE) and the server validates tokens
- [ ] **Phase 3: Households, Membership & Server Data Foundation** — Users create/join households; server enforces household scope
- [ ] **Phase 4: Sync Engine Skeleton** — Offline-first read/write with outbox-backed LWW sync on a sentinel table
@@ -49,7 +49,16 @@
3. iOS `gradle.properties` carry `kotlin.native.binary.objcDisposeOnMain=false` and `kotlin.native.binary.gc=cms`; a debug launch on simulator boots without warnings about legacy memory-management flags.
4. `build-logic/` convention plugins apply the Kotlin/Compose/test configuration to every module — adding a new module requires only applying a convention plugin, not copying compiler args.
5. `shared/commonMain` contains only domain models + serializable DTOs; no Ktor, Compose, or SQLDelight imports appear anywhere under `shared/`.
-**Plans:** TBD
+**Plans:** 7 plans
+
+Plans:
+- [ ] 01-01-PLAN.md — Version catalog extensions (Koin/Kermit/Spotless/Flyway/Postgres) + iOS K/N flags + verify-*.sh invariant scripts
+- [ ] 01-02-PLAN.md — build-logic/ included build with 5 precompiled plugins (recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server) + root settings.gradle.kts includeBuild wiring
+- [ ] 01-03-PLAN.md — Module refactor: composeApp/shared/server build.gradle.kts apply recipe.* conventions; drop js target; enable explicitApi() on shared/
+- [ ] 01-04-PLAN.md — Koin + Kermit bootstrap across all 4 platforms (commonMain Koin.kt/AppModule.kt/Logging.kt; iOS KoinIos.kt bridge; Android MainApplication.kt + manifest; JVM/Wasm main() rewrites; iOSApp.swift wiring)
+- [ ] 01-05-PLAN.md — Server /health + Flyway bootstrap + HOCON config (application.conf, Database.kt with fail-loud contract, db/migration/.gitkeep, ApplicationTest.kt covers /health without Postgres)
+- [ ] 01-06-PLAN.md — docker-compose.yml (postgres:16) + README.md Local development section (drops js docs)
+- [ ] 01-07-PLAN.md — shared/ package scaffold + full green-build gate (spotlessApply, verify-*.sh, ./gradlew build, ./gradlew check)
**UI hint:** no
**Research flag:** no
@@ -203,7 +212,7 @@
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Project Infrastructure & Module Wiring | 0/0 | Not started | - |
+| 1. Project Infrastructure & Module Wiring | 7/7 | Complete | 2026-04-24 |
| 2. Authentication Foundation | 0/0 | Not started | - |
| 3. Households, Membership & Server Data Foundation | 0/0 | Not started | - |
| 4. Sync Engine Skeleton | 0/0 | Not started | - |
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 261057c..4966421 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -1,3 +1,18 @@
+---
+gsd_state_version: 1.0
+milestone: v1.0
+milestone_name: milestone
+current_plan: 7
+status: phase-complete
+last_updated: "2026-04-24T18:56:34.969Z"
+progress:
+ total_phases: 11
+ completed_phases: 1
+ total_plans: 7
+ completed_plans: 7
+ percent: 100
+---
+
# Project State: Recipe
**Project reference:** `.planning/PROJECT.md`
@@ -10,11 +25,13 @@
## Current Position
-**Current focus:** Phase 1: Project Infrastructure & Module Wiring
-**Current plan:** —
-**Status:** Roadmap created; no plan started yet
-**Phase progress:** 0 / 11 phases complete
-**Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0%
+Phase: 01 — Project Infrastructure & Module Wiring — COMPLETE
+Plan: 7 of 7
+**Current focus:** Phase 1 automated gate complete
+**Current plan:** 7
+**Status:** Phase 1 complete; ready to plan Phase 2
+**Phase progress:** 1 / 11 phases complete
+**Progress bar:** `██░░░░░░░░░░░░░░░░░░` 9%
## Performance Metrics
@@ -23,8 +40,8 @@
| Phases planned | 11 |
| v1 requirements | 72 |
| Coverage | 100% |
-| Phases complete | 0 |
-| Plans complete | 0 |
+| Phases complete | 1 |
+| Plans complete | 7 |
## Accumulated Context
@@ -34,7 +51,7 @@ All locked tech-stack decisions are captured in `.planning/PROJECT.md § Key Dec
### Open todos
-- None yet — first action is `/gsd-plan-phase 1`.
+- None.
### Blockers
@@ -42,14 +59,17 @@ All locked tech-stack decisions are captured in `.planning/PROJECT.md § Key Dec
## Session Continuity
-**Last session:** Roadmapping session, 2026-04-23. Produced `ROADMAP.md` with 11 phases and `STATE.md`; updated `REQUIREMENTS.md` traceability table.
+**Last session:** Completed 01-07-PLAN.md
-**Next action:** `/gsd-plan-phase 1` — decompose Phase 1 (Project Infrastructure & Module Wiring) into plans.
+**Next action:** `/gsd-discuss-phase 2` or `/gsd-plan-phase 2` — Authentication Foundation.
**Research flags to revisit during phase planning:**
+
- Phase 2 (Auth): Authentik-specific OIDC setup; iOS OIDC wrapper library choice; token refresh behavior.
- Phase 4 (SyncEngine): concrete cursor format, outbox schema ordering guarantees, retry/backoff policy.
- Phase 10 (UI chrome): current Haze CMP-iOS perf on iPhone 11/12-era hardware; liquid-glass approximation patterns.
---
-*Last updated: 2026-04-23*
+*Last updated: 2026-04-24*
+
+**Planned Phase:** 1 (Project Infrastructure & Module Wiring) — 7 plans — 2026-04-24T16:07:36.289Z
diff --git a/.planning/config.json b/.planning/config.json
index b026c69..0f7ec11 100644
--- a/.planning/config.json
+++ b/.planning/config.json
@@ -27,7 +27,8 @@
"discuss_mode": "discuss",
"skip_discuss": false,
"code_review": true,
- "code_review_depth": "standard"
+ "code_review_depth": "standard",
+ "_auto_chain_active": false
},
"hooks": {
"context_warnings": true
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-01-PLAN.md b/.planning/phases/01-project-infrastructure-module-wiring/01-01-PLAN.md
new file mode 100644
index 0000000..5cb78a1
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-01-PLAN.md
@@ -0,0 +1,342 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - gradle/libs.versions.toml
+ - gradle.properties
+ - tools/verify-no-version-literals.sh
+ - tools/verify-shared-pure.sh
+ - tools/verify-ios-flags.sh
+autonomous: true
+requirements: [INFRA-01, INFRA-03]
+requirements_addressed: [INFRA-01, INFRA-03]
+
+must_haves:
+ truths:
+ - "gradle/libs.versions.toml is the sole source of library/plugin versions (D-09 / INFRA-01 SC#2)"
+ - "iOS K/N binary flags kotlin.native.binary.gc=cms and kotlin.native.binary.objcDisposeOnMain=false are set in gradle.properties (D-18 / INFRA-03)"
+ - "Shell-based invariant checks (no-version-literals, shared-pure, ios-flags) are executable and fail-loud"
+ artifacts:
+ - path: "gradle/libs.versions.toml"
+ provides: "Version + library + plugin aliases for Koin, Kermit, Spotless, Flyway, PostgreSQL JDBC, Ktor content-negotiation, Ktor JSON serializer"
+ contains: "koin = ", "kermit = ", "spotless = ", "flyway = ", "postgresql ="
+ - path: "gradle.properties"
+ provides: "iOS K/N binary flags"
+ contains: "kotlin.native.binary.gc=cms", "kotlin.native.binary.objcDisposeOnMain=false"
+ - path: "tools/verify-no-version-literals.sh"
+ provides: "Invariant check — no numeric version literals outside catalog in any *.gradle.kts (except build-logic/build.gradle.kts bootstrap coordinates)"
+ - path: "tools/verify-shared-pure.sh"
+ provides: "Invariant check — shared/src/commonMain must not import Ktor / Compose / SQLDelight"
+ - path: "tools/verify-ios-flags.sh"
+ provides: "Invariant check — both iOS K/N flags present in gradle.properties"
+ key_links:
+ - from: "build-logic/ (Plan 02)"
+ to: "gradle/libs.versions.toml"
+ via: "VersionCatalogsExtension.named(\"libs\").findLibrary(...) inside precompiled plugins"
+ pattern: "findLibrary\\(\"koin-core\"\\)"
+ - from: "gradle.properties"
+ to: ":composeApp:linkDebugFrameworkIosSimulatorArm64"
+ via: "Kotlin/Native compiler reads project properties at link time"
+ pattern: "kotlin\\.native\\.binary\\."
+---
+
+
+Extend the Gradle version catalog with every new alias required by Phase 1 (Koin, Kermit, Spotless, Flyway, Postgres JDBC, ktor-serverContentNegotiation, ktor-serializationKotlinxJson), append the two mandatory iOS Kotlin/Native binary flags to `gradle.properties`, and ship three shell-based invariant scripts under `tools/` that Plan 07 will use as phase-gate checks.
+
+Purpose: This plan creates the **foundation** on which every other Phase 1 plan rests. Without these catalog entries, `build-logic/` (Plan 02) cannot resolve `findLibrary("koin-core")`; without the iOS flags, INFRA-03 fails silently. The verification scripts are required by 01-VALIDATION.md Wave 0 — every subsequent plan's `` block calls one of them.
+
+Output: An extended `gradle/libs.versions.toml` (additive only, no version bumps to existing entries), extended `gradle.properties` with exactly two new lines, and three executable `.sh` scripts under a new `tools/` directory.
+
+
+
+@$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/REQUIREMENTS.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
+@gradle/libs.versions.toml
+@gradle.properties
+@CLAUDE.md
+
+
+
+
+From gradle/libs.versions.toml (current state, to extend):
+```toml
+[versions]
+kotlin = "2.3.20"
+ktor = "3.4.1"
+composeMultiplatform = "1.10.3"
+# (plus agp, androidx-*, composeHotReload, junit, kotlinx-coroutines, logback, material3)
+
+[libraries]
+# Existing: kotlin-test, kotlin-testJunit, junit, androidx-*, compose-*, kotlinx-coroutinesSwing,
+# logback, ktor-serverCore, ktor-serverNetty, ktor-serverTestHost
+
+[plugins]
+# Existing: androidApplication, androidLibrary, composeHotReload, composeMultiplatform,
+# composeCompiler, kotlinJvm, ktor, kotlinMultiplatform
+```
+
+From gradle.properties (current state — 10 lines of Kotlin + Gradle + Android config):
+```properties
+kotlin.code.style=official
+kotlin.daemon.jvmargs=-Xmx3072M
+org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
+org.gradle.configuration-cache=true
+org.gradle.caching=true
+android.nonTransitiveRClass=true
+android.useAndroidX=true
+```
+
+
+
+
+
+
+ Task 1: Extend gradle/libs.versions.toml with Phase 1 aliases
+ gradle/libs.versions.toml
+
+ - gradle/libs.versions.toml (see current state of versions/libraries/plugins tables)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 110-175 (§ Standard Stack + Installation TOML fragments)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 446-490 (delta blocks for [versions] / [libraries] / [plugins])
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-09 (catalog-only hard rule), D-14 (Koin deps needed), D-15 (Kermit), D-10 (Spotless), D-16 (Flyway + Postgres + content-negotiation)
+
+
+ Extend `gradle/libs.versions.toml` with the new aliases for Phase 1. Preserve every existing entry verbatim (do NOT rename, remove, or bump any existing version).
+
+ Append the following to `[versions]`, in the existing alphabetical-ish order:
+
+ ```toml
+ flyway = "12.4.0"
+ kermit = "2.1.0"
+ koin = "4.2.1"
+ kotlinx-serialization = "1.7.3"
+ postgresql = "42.7.10"
+ spotless = "8.4.0"
+ ```
+
+ Append the following to `[libraries]`:
+
+ ```toml
+ # Koin (client DI — D-14)
+ koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
+ koin-core = { module = "io.insert-koin:koin-core" }
+ koin-compose = { module = "io.insert-koin:koin-compose" }
+ koin-composeViewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
+ koin-android = { module = "io.insert-koin:koin-android" }
+
+ # Kermit (client logger — D-15)
+ kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
+
+ # Server: Ktor content-negotiation + JSON serializer + Flyway + Postgres (D-16)
+ ktor-serverContentNegotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" }
+ ktor-serializationKotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" }
+ flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" }
+ flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" }
+ postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
+ ```
+
+ Append the following to `[plugins]`:
+
+ ```toml
+ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
+ flywayPlugin = { id = "org.flywaydb.flyway", version.ref = "flyway" }
+ ```
+
+ IMPORTANT invariants:
+ - `koin-core`, `koin-compose`, `koin-compose-viewmodel`, `koin-android` have NO `version.ref` — they are BOM-managed by `koin-bom`.
+ - `kotlin-test` is already in the catalog (line 22) — do NOT re-add.
+ - Do NOT bump any existing version alias (kotlin, ktor, composeMultiplatform, logback, etc.).
+ - The `koin-composeViewmodel` alias name uses camelCase (Gradle converts dashes-to-dots for accessors, but camelCase preserves `koin.composeViewmodel.get()`).
+
+
+ grep -E '^(flyway|kermit|koin|kotlinx-serialization|postgresql|spotless)\s*=' gradle/libs.versions.toml | wc -l | grep -q '^6$' && grep -E '^koin-bom\s*=' gradle/libs.versions.toml && grep -E '^koin-core\s*=' gradle/libs.versions.toml && grep -E '^koin-compose\s*=' gradle/libs.versions.toml && grep -E '^koin-composeViewmodel\s*=' gradle/libs.versions.toml && grep -E '^koin-android\s*=' gradle/libs.versions.toml && grep -E '^kermit\s*=' gradle/libs.versions.toml && grep -E '^ktor-serverContentNegotiation\s*=' gradle/libs.versions.toml && grep -E '^ktor-serializationKotlinxJson\s*=' gradle/libs.versions.toml && grep -E '^flyway-core\s*=' gradle/libs.versions.toml && grep -E '^flyway-database-postgresql\s*=' gradle/libs.versions.toml && grep -E '^postgresql\s*=' gradle/libs.versions.toml && grep -E '^spotless\s*=\s*\{\s*id\s*=' gradle/libs.versions.toml && grep -E '^flywayPlugin\s*=\s*\{\s*id\s*=' gradle/libs.versions.toml
+
+
+ - `grep -E '^kotlin\s*=\s*"2\.3\.20"' gradle/libs.versions.toml` returns exactly 1 line (existing, unmodified)
+ - `grep -E '^ktor\s*=\s*"3\.4\.1"' gradle/libs.versions.toml` returns exactly 1 line (existing, unmodified)
+ - `grep -E '^koin\s*=\s*"4\.2\.1"' gradle/libs.versions.toml` returns exactly 1 line (new)
+ - `grep -E '^kermit\s*=\s*"2\.1\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
+ - `grep -E '^spotless\s*=\s*"8\.4\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
+ - `grep -E '^flyway\s*=\s*"12\.4\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
+ - `grep -E '^postgresql\s*=\s*"42\.7\.10"' gradle/libs.versions.toml` returns exactly 1 line (new)
+ - `grep -c '^koin-' gradle/libs.versions.toml` returns `5` (koin-bom, koin-core, koin-compose, koin-composeViewmodel, koin-android)
+ - `grep -c '^flyway-' gradle/libs.versions.toml` returns `2` (flyway-core, flyway-database-postgresql)
+ - `grep -E '^\s*module\s*=\s*"io.insert-koin:koin-core"' gradle/libs.versions.toml` returns 1 line with NO `version.ref` attribute on same line (BOM-managed)
+
+ All Phase 1 catalog aliases present; no existing aliases modified; file parses as valid TOML.
+
+
+
+ Task 2: Append iOS K/N binary flags to gradle.properties
+ gradle.properties
+
+ - gradle.properties (see current 10-line content)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1082-1107 (§ `gradle.properties` — iOS binary flags — exact content to append)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-18 (INFRA-03, PITFALL #1)
+ - CLAUDE.md convention #7 (iOS binary flags on day 1)
+
+
+ Append the following 5 lines to `gradle.properties` exactly as shown (including the blank separator line and both comment lines). Do NOT modify any existing line:
+
+ ```properties
+
+ # Kotlin/Native iOS (PITFALLS.md #1; D-18; INFRA-03) — MANDATORY day 1
+ # CMS GC + non-main-thread Obj-C deinit to avoid UI-thread pause spikes in Compose Multiplatform.
+ kotlin.native.binary.gc=cms
+ kotlin.native.binary.objcDisposeOnMain=false
+ ```
+
+ IMPORTANT:
+ - Place AT THE END of the file (append). The existing `android.useAndroidX=true` stays as the last non-iOS line.
+ - Use EXACTLY the property keys `kotlin.native.binary.gc` and `kotlin.native.binary.objcDisposeOnMain`. Do not add quotes, spaces, or alternate spellings (the K/N compiler reads these keys literally).
+ - Value `cms` is lowercase. Value `false` is lowercase.
+
+
+ grep -E '^kotlin\.native\.binary\.gc=cms$' gradle.properties | wc -l | grep -q '^1$' && grep -E '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties | wc -l | grep -q '^1$'
+
+
+ - `grep -cE '^kotlin\.native\.binary\.gc=cms$' gradle.properties` returns `1`
+ - `grep -cE '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties` returns `1`
+ - `grep -c '^kotlin\.code\.style=official$' gradle.properties` returns `1` (unmodified existing)
+ - `grep -c '^android\.useAndroidX=true$' gradle.properties` returns `1` (unmodified existing)
+ - No duplicate of either flag (run grep twice — expect `1` each time, not `2`)
+
+ Both iOS K/N flags present once; original 10 lines unchanged.
+
+
+
+ Task 3: Create verify-*.sh invariant scripts under tools/
+ tools/verify-no-version-literals.sh, tools/verify-shared-pure.sh, tools/verify-ios-flags.sh
+
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1174-1218 (§ tools/verify-*.sh — canonical shell sketches)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 1174-1218 (same scripts, same content — Pattern Map confirms no in-repo analog)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md lines 62-79 (Wave 0 Requirements — these three scripts gate every task's `` check)
+
+
+ Create the three executable bash scripts under `tools/` (create the directory — it does not exist yet). Each must be marked executable (`chmod +x`).
+
+ **File 1: `tools/verify-no-version-literals.sh`** (enforces D-09 / INFRA-01 SC#2):
+
+ ```sh
+ #!/usr/bin/env bash
+ # Enforces INFRA-01 SC#2 / D-09: no literal version strings outside catalog.
+ # Scans every *.gradle.kts for numeric version literals (e.g. version = "1.2.3"),
+ # excluding build-logic/build.gradle.kts which needs literal asDependency() coordinates.
+ set -euo pipefail
+ VIOLATIONS=$(grep -rn -E 'version[[:space:]]*=[[:space:]]*"[0-9]' --include='*.gradle.kts' . 2>/dev/null | grep -v 'build-logic/build.gradle.kts' || true)
+ if [ -n "$VIOLATIONS" ]; then
+ echo "ERROR: version literals found outside catalog:" >&2
+ echo "$VIOLATIONS" >&2
+ exit 1
+ fi
+ echo "OK: no version literals outside catalog."
+ ```
+
+ **File 2: `tools/verify-shared-pure.sh`** (enforces INFRA-06 / D-19):
+
+ ```sh
+ #!/usr/bin/env bash
+ # Enforces INFRA-06 / D-19: shared/commonMain must not import Ktor, Compose, SQLDelight.
+ # Runs grep against shared/src/commonMain/ only. Allowed imports: kotlin.*, kotlinx.serialization, kotlinx.datetime.
+ set -euo pipefail
+ if [ ! -d shared/src/commonMain ]; then
+ echo "OK: shared/src/commonMain does not exist yet (pre-scaffold)."
+ exit 0
+ fi
+ VIOLATIONS=$(grep -rn -E '^import[[:space:]]+(io\.ktor|androidx\.compose|org\.jetbrains\.compose|app\.cash\.sqldelight)' shared/src/commonMain/ 2>/dev/null || true)
+ if [ -n "$VIOLATIONS" ]; then
+ echo "ERROR: shared/commonMain has forbidden imports:" >&2
+ echo "$VIOLATIONS" >&2
+ exit 1
+ fi
+ echo "OK: shared/commonMain is pure."
+ ```
+
+ **File 3: `tools/verify-ios-flags.sh`** (enforces INFRA-03 / D-18):
+
+ ```sh
+ #!/usr/bin/env bash
+ # Enforces INFRA-03 / D-18: iOS K/N flags present in gradle.properties.
+ set -euo pipefail
+ grep -q '^kotlin\.native\.binary\.gc=cms$' gradle.properties || { echo "MISSING: kotlin.native.binary.gc=cms" >&2; exit 1; }
+ grep -q '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties || { echo "MISSING: kotlin.native.binary.objcDisposeOnMain=false" >&2; exit 1; }
+ echo "OK: iOS binary flags present."
+ ```
+
+ After writing all three files, run: `chmod +x tools/verify-no-version-literals.sh tools/verify-shared-pure.sh tools/verify-ios-flags.sh`.
+
+ IMPORTANT:
+ - Use `#!/usr/bin/env bash` (not `#!/bin/sh`) — `set -euo pipefail` requires bash semantics.
+ - `tools/verify-shared-pure.sh` deliberately returns 0 if `shared/src/commonMain` does not exist (pre-scaffold state). This lets Plan 07 run the script before Plan 07 itself creates the scaffold.
+ - `tools/verify-no-version-literals.sh` excludes `build-logic/build.gradle.kts` (its `asDependency()` trick requires literal plugin version coordinates — D-09 acknowledged exception).
+
+
+ test -x tools/verify-no-version-literals.sh && test -x tools/verify-shared-pure.sh && test -x tools/verify-ios-flags.sh && bash tools/verify-ios-flags.sh && bash tools/verify-shared-pure.sh && bash tools/verify-no-version-literals.sh
+
+
+ - `test -f tools/verify-no-version-literals.sh && test -x tools/verify-no-version-literals.sh` succeeds
+ - `test -f tools/verify-shared-pure.sh && test -x tools/verify-shared-pure.sh` succeeds
+ - `test -f tools/verify-ios-flags.sh && test -x tools/verify-ios-flags.sh` succeeds
+ - `bash tools/verify-ios-flags.sh` exits 0 and prints `OK: iOS binary flags present.` (proves Task 2 wrote flags)
+ - `bash tools/verify-shared-pure.sh` exits 0 (current `shared/src/commonMain/kotlin/dev/ulfrx/recipe/` has only Greeting.kt/Platform.kt/Constants.kt — no ktor/compose imports)
+ - `bash tools/verify-no-version-literals.sh` exits 0 (current *.gradle.kts files use `libs.plugins.*` aliases — no literal versions)
+ - Each script has `#!/usr/bin/env bash` as line 1
+ - Each script uses `set -euo pipefail`
+
+ Three executable verification scripts exist, each runs green against the current repo state.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| developer → Gradle build | Local-only; Gradle reads `libs.versions.toml` + `gradle.properties` verbatim. No untrusted input. |
+| Gradle → Maven Central + Gradle Plugin Portal | Existing repository declarations in `settings.gradle.kts` (Plan 03 doesn't change them). Pinned versions via catalog reduce supply-chain drift. |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-01-01 | Tampering (supply chain) | `gradle/libs.versions.toml` new entries | mitigate | All new version refs are pinned to specific stable releases (`koin = "4.2.1"`, `kermit = "2.1.0"`, `flyway = "12.4.0"`, `spotless = "8.4.0"`, `postgresql = "42.7.10"`) — no version ranges, no `latest.release`. Gradle verifies SHA-256 via `gradle/verification-metadata.xml` if enabled in later phases. |
+| T-01-01-02 | Tampering | `tools/*.sh` scripts | accept | Scripts live in repo and run locally; their only effect is exit 0/1. Read `gradle.properties` and `*.gradle.kts` only — no network I/O, no write. Risk = low. |
+| T-01-01-03 | Information Disclosure | `gradle.properties` iOS flags | accept | Flag values (`cms`, `false`) are build configuration, not secrets. Public in every iOS KMP tutorial. |
+| T-01-01-04 | Denial of Service | wrong catalog syntax breaks build | mitigate | Task 1 `` greps for exact alias presence; Wave 2 plans that consume the catalog will fail fast if an alias is misspelled. |
+
+
+
+Phase-level verification for this plan:
+- All three `tools/verify-*.sh` scripts run green against the post-plan repo.
+- `gradle/libs.versions.toml` parses (Gradle will surface a TOML parse error at next `./gradlew` invocation in Plan 02).
+- `gradle.properties` has exactly two new iOS K/N flag lines and is otherwise byte-identical to its pre-plan content.
+
+No Gradle build is expected to run fully in this plan — we have not yet scaffolded `build-logic/` (Plan 02) nor refactored modules (Plan 03), so `./gradlew build` would fail to resolve the new library aliases. Catalog additions ARE safe for Gradle configuration though (unused entries are inert).
+
+
+
+- `tools/verify-ios-flags.sh` exits 0
+- `tools/verify-no-version-literals.sh` exits 0
+- `tools/verify-shared-pure.sh` exits 0
+- Catalog contains 6 new `[versions]` keys (flyway, kermit, koin, kotlinx-serialization, postgresql, spotless)
+- Catalog contains 10 new `[libraries]` entries (5 koin-*, kermit, 2 ktor-*, 2 flyway-*, postgresql)
+- Catalog contains 2 new `[plugins]` entries (spotless, flywayPlugin)
+
+
+
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-01-SUMMARY.md b/.planning/phases/01-project-infrastructure-module-wiring/01-01-SUMMARY.md
new file mode 100644
index 0000000..9861801
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-01-SUMMARY.md
@@ -0,0 +1,154 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 01
+subsystem: infra
+tags: [gradle, version-catalog, kotlin-native, ios-binary-flags, bash, invariants, koin, kermit, flyway, postgresql, spotless, ktor]
+
+# Dependency graph
+requires: []
+provides:
+ - Gradle version catalog extended with Koin (BOM + core/compose/composeViewmodel/android), Kermit, Spotless, Flyway (core + postgresql), Postgres JDBC, Ktor content-negotiation + kotlinx-json serializer
+ - kotlinx-serialization = 1.7.3 version alias (kept even though no library wires it in Plan 01 — Phase 2+ wire Ktor plugins using this pin)
+ - iOS K/N binary flags kotlin.native.binary.gc=cms + kotlin.native.binary.objcDisposeOnMain=false in gradle.properties
+ - tools/verify-no-version-literals.sh (D-09 invariant check)
+ - tools/verify-shared-pure.sh (INFRA-06 / D-19 invariant check — tolerant of pre-scaffold shared/commonMain)
+ - tools/verify-ios-flags.sh (INFRA-03 / D-18 invariant check)
+affects: [01-02-build-logic, 01-03-module-wiring, 01-04-compose-app, 01-05-server, 01-06-shared, 01-07-validation, 02-auth, 10-ui-chrome, 11-localization-deployment]
+
+# Tech tracking
+tech-stack:
+ added:
+ - Koin 4.2.1 (BOM + 4 consumed modules)
+ - Kermit 2.1.0
+ - Spotless 8.4.0 (plugin)
+ - Flyway 12.4.0 (core + database-postgresql module + Gradle plugin)
+ - PostgreSQL JDBC 42.7.10
+ - Ktor server content-negotiation + kotlinx-json serializer (version.ref = existing ktor 3.4.1)
+ - kotlinx-serialization = 1.7.3 version alias (no library entry yet — pre-wire for ktor serializer which derives its version from ktor)
+ patterns:
+ - "Catalog-only versioning: no numeric version literals in *.gradle.kts outside build-logic/ (D-09 / INFRA-01 SC#2)"
+ - "BOM-managed Koin libs omit version.ref (koin-core/koin-compose/koin-composeViewmodel/koin-android pinned via koin-bom)"
+ - "Fail-loud shell invariants under tools/ — every Phase 1 plan's block calls one of these three scripts"
+
+key-files:
+ created:
+ - tools/verify-no-version-literals.sh
+ - tools/verify-shared-pure.sh
+ - tools/verify-ios-flags.sh
+ modified:
+ - gradle/libs.versions.toml
+ - gradle.properties
+
+key-decisions:
+ - "Refined verify-no-version-literals.sh to exclude top-level project-version assignments (^version = \"x.y.z\") — these are Gradle artifact metadata, not library-version pins. D-09 guards dependencies, not project identity. server/build.gradle.kts:8 keeps its project version."
+
+patterns-established:
+ - "Pattern 1: All Phase 1+ library/plugin versions declared ONLY in gradle/libs.versions.toml; build scripts reference via libs.* accessors"
+ - "Pattern 2: iOS Kotlin/Native binary flags live in gradle.properties — single file, compiler reads verbatim at link time"
+ - "Pattern 3: Invariant checks as bash scripts under tools/; shebang #!/usr/bin/env bash; set -euo pipefail; fail-loud on violation, silent-pass on clean"
+ - "Pattern 4: Pre-scaffold tolerance — verify-shared-pure.sh exits 0 if shared/src/commonMain doesn't exist (lets invariant scripts run before Plan 07 scaffolds)"
+
+requirements-completed: [INFRA-01, INFRA-03]
+
+# Metrics
+duration: 4min
+completed: 2026-04-24
+---
+
+# Phase 01 Plan 01: Foundations Summary
+
+**Gradle version catalog extended with 6 versions / 11 libraries / 2 plugins (Koin + Kermit + Spotless + Flyway + Postgres + Ktor content-negotiation), iOS K/N binary flags (gc=cms + objcDisposeOnMain=false) added to gradle.properties, and three tools/verify-*.sh invariant scripts shipped — the foundation every remaining Phase 1 plan leans on.**
+
+## Performance
+
+- **Duration:** 4 min
+- **Started:** 2026-04-24T16:12:45Z
+- **Completed:** 2026-04-24T16:16:53Z
+- **Tasks:** 3
+- **Files modified:** 2 (catalog + gradle.properties)
+- **Files created:** 3 (tools/verify-*.sh)
+
+## Accomplishments
+
+- **Version catalog now covers every Phase 1+ library and plugin**, including BOM-managed Koin modules (omitting `version.ref` by design) and fine-grained Ktor server-side JSON plumbing. No existing version ref was bumped; additive-only per D-09.
+- **iOS K/N binary flags wired on day 1** (gc=cms + objcDisposeOnMain=false), closing PITFALL #1 before any iOS framework is linked.
+- **Three invariant scripts ship green** — `verify-ios-flags.sh`, `verify-shared-pure.sh`, `verify-no-version-literals.sh` — exit 0 against current repo state, ready to gate every subsequent plan's automated checks.
+
+## Task Commits
+
+Each task committed atomically on this worktree branch:
+
+1. **Task 1: Extend gradle/libs.versions.toml with Phase 1 aliases** — `b609cb6` (feat)
+2. **Task 2: Append iOS K/N binary flags to gradle.properties** — `d873c31` (feat)
+3. **Task 3: Create verify-*.sh invariant scripts under tools/** — `aaa8042` (feat)
+
+_No TDD for this plan — all tasks are config/scaffold, not behavior._
+
+## Files Created/Modified
+
+### Created
+- `tools/verify-no-version-literals.sh` — Grep `*.gradle.kts` for `version = "[0-9]..."`; skip `build-logic/build.gradle.kts` (legitimate plugin-dep literals) and top-level project-version assignments (artifact metadata, not library pins).
+- `tools/verify-shared-pure.sh` — Grep `shared/src/commonMain/` for imports from `io.ktor`, `androidx.compose`, `org.jetbrains.compose`, `app.cash.sqldelight`. Exits 0 if the directory doesn't exist yet (Plan 07 hasn't scaffolded it).
+- `tools/verify-ios-flags.sh` — Grep `gradle.properties` for both K/N flags; fail with a clear MISSING: line if either absent.
+
+### Modified
+- `gradle/libs.versions.toml` — +6 versions (flyway, kermit, koin, kotlinx-serialization, postgresql, spotless); +11 libraries (5 koin-*, kermit, 2 ktor-server-*, 2 flyway-*, postgresql); +2 plugins (spotless, flywayPlugin). 24 / 33 / 10 totals after edit.
+- `gradle.properties` — +5 lines (blank separator + comment + comment + 2 K/N flags) appended after existing Android block. Original 10 lines unchanged.
+
+## Decisions Made
+
+- **Refined verify-no-version-literals.sh script semantics** — The plan's canonical script (from 01-RESEARCH.md lines 1174–1218 and 01-PATTERNS.md lines 446–490) excluded only `build-logic/build.gradle.kts`. Running it against the current repo tripped on `server/build.gradle.kts:8: version = "1.0.0"` — the Ktor template's project-version property. Per D-09, the invariant targets **library/plugin** version literals, not project/artifact metadata. I added a second, narrow exclusion: lines where the matched `version = "..."` begins at column 0 (unindented project-version assignments). Library and plugin version literals always appear inside a `dependencies { }` or `plugins { }` block and are therefore indented, so they remain caught. Sanity-check: the script still flags a synthetic `dependencies { implementation("x:y") { version = "9.9.9" } }` as a violation.
+
+## Deviations from Plan
+
+### Auto-fixed Issues
+
+**1. [Rule 3 - Blocking] Refined verify-no-version-literals.sh to not fire on project-version metadata**
+- **Found during:** Task 3 (running the script for the first time)
+- **Issue:** The canonical script in the plan (quoted verbatim from 01-RESEARCH.md) exit-1'd on `server/build.gradle.kts:8: version = "1.0.0"`. That line is the Gradle project-version property (artifact name metadata), not a library-version pin. The plan's acceptance criterion ("`bash tools/verify-no-version-literals.sh` exits 0 today") cannot be satisfied without either removing the project version or refining the script. D-09 (CONTEXT.md line 32) says the rule is "no library versions outside catalog" — project version is out of scope for D-09.
+- **Fix:** Added a second `grep -v` to exclude lines matching `:[0-9]+:version[[:space:]]*=[[:space:]]*"[0-9]` (unindented top-level project-version). Library/plugin version literals in Gradle DSL are always indented inside a block, so they remain caught. Updated the script's header comment to document the refinement rationale.
+- **Files modified:** `tools/verify-no-version-literals.sh`
+- **Verification:** Script exits 0 against current repo state; synthetic indented `version = "9.9.9"` test case still trips the script with exit 1. Both conditions tested.
+- **Committed in:** `aaa8042` (Task 3 commit — the refined script is the only shipped version; no redundant fix-up commit).
+
+---
+
+**Total deviations:** 1 auto-fixed (Rule 3 — blocking)
+**Impact on plan:** Minimal. The refinement strengthens the script's semantic correctness (targets library/plugin pins, not project identity). Success criteria and all acceptance criteria still pass. No additional tasks; no scope creep.
+
+## Issues Encountered
+
+- **Initial worktree base mismatch** — Worktree branch HEAD was `0ca22f9e` (a later commit in the worktree's own history), not the expected `875055a` base. The `` guard caught it and reset to `875055a` before any work. All three task commits therefore sit cleanly on the required base.
+- **Planner arithmetic off-by-one** — Plan success criteria say "10 new [libraries] entries"; the plan's own enumeration lists 11 (5 koin + kermit + 2 ktor + 2 flyway + postgresql). I shipped all 11 explicitly named entries. This is a planner-side typo, not a deviation.
+
+## User Setup Required
+
+None. This plan is pure build configuration — no secrets, no external services, no dashboard config.
+
+## Next Phase Readiness
+
+- **Plan 02 (build-logic/) unblocked** — `VersionCatalogsExtension.named("libs").findLibrary("koin-core")` etc. will resolve in precompiled plugins; every alias the downstream plans need is now present.
+- **Plan 03+ module build files unblocked** — modules can reference `libs.koin.core`, `libs.kermit`, `libs.ktor.serverContentNegotiation`, `libs.flyway.core`, `libs.flyway.database.postgresql`, `libs.postgresql` via type-safe accessors.
+- **Plan 07 (validation) unblocked** — the three `tools/verify-*.sh` scripts are the Wave 0 gate it enumerates.
+- **No blockers.** `./gradlew build` is NOT expected to pass until Plan 02 wires up `build-logic/` and Plan 03 refactors module build scripts — that's by design and stated in this plan's `` block.
+
+## Self-Check: PASSED
+
+Verification of claims in this summary:
+
+**Created files exist:**
+- `tools/verify-no-version-literals.sh` — FOUND + executable
+- `tools/verify-shared-pure.sh` — FOUND + executable
+- `tools/verify-ios-flags.sh` — FOUND + executable
+
+**Commits exist in branch history:**
+- `b609cb6` — FOUND (feat(01-01): extend version catalog with Phase 1 aliases)
+- `d873c31` — FOUND (feat(01-01): add iOS Kotlin/Native binary flags to gradle.properties)
+- `aaa8042` — FOUND (feat(01-01): add Phase 1 invariant verification scripts)
+
+**All three invariant scripts exit 0 against the current repo state.** All success criteria from the plan pass.
+
+---
+
+*Phase: 01-project-infrastructure-module-wiring*
+*Completed: 2026-04-24*
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-02-PLAN.md b/.planning/phases/01-project-infrastructure-module-wiring/01-02-PLAN.md
new file mode 100644
index 0000000..498054b
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-02-PLAN.md
@@ -0,0 +1,587 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 02
+type: execute
+wave: 2
+depends_on: [01]
+files_modified:
+ - build-logic/settings.gradle.kts
+ - build-logic/build.gradle.kts
+ - build-logic/src/main/kotlin/recipe.quality.gradle.kts
+ - build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts
+ - build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts
+ - build-logic/src/main/kotlin/recipe.android.application.gradle.kts
+ - build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
+ - settings.gradle.kts
+ - build.gradle.kts
+autonomous: true
+requirements: [INFRA-02]
+requirements_addressed: [INFRA-02]
+
+must_haves:
+ truths:
+ - "build-logic/ is an included build resolved via pluginManagement.includeBuild (PITFALL #9)"
+ - "5 precompiled script plugins exist under build-logic/src/main/kotlin/: recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server (D-06)"
+ - "Each precompiled plugin reads versions via extensions.getByType().named(\"libs\") (PITFALL #1)"
+ - "recipe.kotlin.multiplatform locks the D-05 target matrix (androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs) + JVM toolchain 21 + framework basename 'ComposeApp' + Koin/Kermit/kotlin-test deps + allWarningsAsErrors"
+ - "recipe.compose.multiplatform layers on recipe.kotlin.multiplatform (does NOT re-declare KMP plugin — PITFALL #2)"
+ - "recipe.jvm.server uses quoted dependency configurations (\"implementation\"(...) — quoted-config footgun)"
+ - "settings.gradle.kts places includeBuild(\"build-logic\") INSIDE pluginManagement { } block (PITFALL #9)"
+ artifacts:
+ - path: "build-logic/settings.gradle.kts"
+ provides: "Included-build settings with shared catalog access (from files(\"../gradle/libs.versions.toml\"))"
+ - path: "build-logic/build.gradle.kts"
+ provides: "kotlin-dsl plugin + compileOnly(asDependency()) entries for every alias-based plugin referenced by precompiled plugins"
+ - path: "build-logic/src/main/kotlin/recipe.quality.gradle.kts"
+ provides: "Spotless + ktlint + allWarningsAsErrors safety net (D-10 / D-11)"
+ - path: "build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts"
+ provides: "D-05 target matrix + JVM toolchain + common deps + allWarningsAsErrors (D-07, D-08, D-11)"
+ - path: "build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts"
+ provides: "Compose MP plugin + hot-reload + Compose deps for commonMain (layered on KMP)"
+ - path: "build-logic/src/main/kotlin/recipe.android.application.gradle.kts"
+ provides: "com.android.application + namespace + SDK versions (composeApp only)"
+ - path: "build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts"
+ provides: "kotlin(jvm) + Ktor + Flyway + server deps (server only)"
+ - path: "settings.gradle.kts"
+ provides: "Root settings with pluginManagement { includeBuild(\"build-logic\") }"
+ - path: "build.gradle.kts"
+ provides: "Root build with apply-false entries for spotless + flywayPlugin (classloader hint)"
+ key_links:
+ - from: "build-logic/src/main/kotlin/recipe.*.gradle.kts"
+ to: "gradle/libs.versions.toml"
+ via: "VersionCatalogsExtension.named(\"libs\")"
+ pattern: "extensions\\.getByType\\(\\)\\.named\\(\"libs\"\\)"
+ - from: "Plan 03 module build files"
+ to: "build-logic/src/main/kotlin/recipe.*.gradle.kts"
+ via: "plugins { id(\"recipe.kotlin.multiplatform\") }"
+ pattern: "id\\(\"recipe\\."
+---
+
+
+Scaffold the `build-logic/` included build with 5 precompiled script plugins (`recipe.quality`, `recipe.kotlin.multiplatform`, `recipe.compose.multiplatform`, `recipe.android.application`, `recipe.jvm.server`) that every module in Plan 03 will apply. Wire the included build into `settings.gradle.kts` via `pluginManagement.includeBuild("build-logic")` and extend the root `build.gradle.kts` with `apply false` declarations for the two new plugins (Spotless + Flyway) so Gradle's classloader resolves them consistently.
+
+Purpose: This is the **dependency root** for every subsequent Phase 1 plan. Plan 03 cannot refactor module builds until these plugins exist. Plan 05 cannot wire Flyway into the server without `recipe.jvm.server`. The design (per D-06) enforces role declarations — `shared/` applies only `recipe.kotlin.multiplatform` + `recipe.quality` and therefore CANNOT pull Compose transitively (INFRA-06).
+
+Output: A fully populated `build-logic/` directory whose included-build settings resolve the parent catalog, a root settings file that finds `recipe.*` plugins by ID, and 5 precompiled plugins whose internals are verbatim (or near-verbatim) copies of 01-RESEARCH.md § Code Examples / § Architecture Patterns.
+
+
+
+@$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
+@settings.gradle.kts
+@build.gradle.kts
+@gradle/libs.versions.toml
+@CLAUDE.md
+
+
+
+
+Plugin applications reference (01-PATTERNS.md and 01-RESEARCH.md):
+- `id("recipe.quality")` → from .gradle.kts file named `recipe.quality.gradle.kts` (Gradle convention)
+- `id("recipe.kotlin.multiplatform")` → `recipe.kotlin.multiplatform.gradle.kts`
+- etc.
+
+Version-catalog access pattern inside precompiled plugins (PITFALL #1, RESEARCH.md lines 362-380):
+```kotlin
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.getByType
+
+val libs = extensions.getByType().named("libs")
+
+// Usage:
+val v = libs.findVersion("kotlin").get().toString()
+val lib = libs.findLibrary("koin-core").get()
+```
+
+Quoted configuration names in precompiled plugin dependencies (RESEARCH.md line 603, Pattern 7):
+```kotlin
+dependencies {
+ "implementation"(libs.findLibrary("ktor-serverCore").get()) // quoted!
+ // NOT: implementation(...) — unresolved reference in precompiled plugin context
+}
+```
+
+The root `settings.gradle.kts` layout required by PITFALL #9 (RESEARCH.md lines 749-767):
+```kotlin
+pluginManagement {
+ includeBuild("build-logic") // MUST be inside pluginManagement { }
+ repositories { ... }
+}
+```
+
+
+
+
+
+
+ Task 1: Scaffold build-logic/ included build + 5 precompiled plugins
+ build-logic/settings.gradle.kts, build-logic/build.gradle.kts, build-logic/src/main/kotlin/recipe.quality.gradle.kts, build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts, build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts, build-logic/src/main/kotlin/recipe.android.application.gradle.kts, build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
+
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 308-605 (§ Pattern 1 through § Pattern 7 — canonical excerpts for every file in this task)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 652-774 (§ Common Pitfalls 1-10 — especially #1 catalog access, #2 double-apply KMP, #3 warnings-as-errors scope, #7 kotlinOptions, #9 includeBuild location, #10 framework basename)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 105-443 (pattern assignments for each build-logic/ file with deltas)
+ - gradle/libs.versions.toml (Plan 01 added these aliases — verify they exist before writing `findLibrary(...)` references)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-06 through D-17 (plugin split, JVM split, warnings-as-errors, Koin deps, Flyway, server scope)
+
+
+ Create the `build-logic/` directory and all 7 files listed in ``. Each file's content comes directly from 01-RESEARCH.md. Use the Write tool for every file (no heredoc).
+
+ ---
+
+ **File 1: `build-logic/settings.gradle.kts`** (01-RESEARCH.md lines 316-331, verbatim):
+
+ ```kotlin
+ dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+ }
+
+ rootProject.name = "build-logic"
+ ```
+
+ ---
+
+ **File 2: `build-logic/build.gradle.kts`** (01-RESEARCH.md lines 333-358, verbatim):
+
+ ```kotlin
+ plugins {
+ `kotlin-dsl`
+ }
+
+ dependencies {
+ compileOnly(libs.plugins.kotlinMultiplatform.asDependency())
+ compileOnly(libs.plugins.androidApplication.asDependency())
+ compileOnly(libs.plugins.composeMultiplatform.asDependency())
+ compileOnly(libs.plugins.composeCompiler.asDependency())
+ compileOnly(libs.plugins.composeHotReload.asDependency())
+ compileOnly(libs.plugins.kotlinJvm.asDependency())
+ compileOnly(libs.plugins.ktor.asDependency())
+ compileOnly(libs.plugins.spotless.asDependency())
+ compileOnly(libs.plugins.flywayPlugin.asDependency())
+ }
+
+ fun Provider.asDependency(): Provider =
+ map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version.requiredVersion}" }
+ ```
+
+ ---
+
+ **File 3: `build-logic/src/main/kotlin/recipe.quality.gradle.kts`** (01-RESEARCH.md lines 483-512 + D-11 safety net):
+
+ ```kotlin
+ plugins {
+ id("com.diffplug.spotless")
+ }
+
+ spotless {
+ kotlin {
+ target("src/**/*.kt")
+ targetExclude("**/build/**", "**/generated/**")
+ ktlint()
+ }
+ kotlinGradle {
+ target("*.gradle.kts")
+ ktlint()
+ }
+ format("markdown") {
+ target("*.md", "docs/**/*.md")
+ endWithNewline()
+ trimTrailingWhitespace()
+ }
+ }
+
+ // D-11 redundancy guard: if a module applies recipe.quality alongside a Kotlin plugin
+ // (multiplatform or jvm), ensure allWarningsAsErrors still applies even if the module
+ // build didn't already configure it. Guarded with plugins.withId so this plugin is
+ // safely composable even when applied alone (no KotlinCompilationTask type available
+ // on the classpath until a Kotlin plugin is present).
+ plugins.withId("org.jetbrains.kotlin.multiplatform") {
+ tasks.withType>().configureEach {
+ compilerOptions {
+ allWarningsAsErrors.set(true)
+ }
+ }
+ }
+ plugins.withId("org.jetbrains.kotlin.jvm") {
+ tasks.withType>().configureEach {
+ compilerOptions {
+ allWarningsAsErrors.set(true)
+ }
+ }
+ }
+ ```
+
+ ---
+
+ **File 4: `build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts`** (01-RESEARCH.md lines 777-835, verbatim — the canonical KMP plugin):
+
+ ```kotlin
+ // build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts
+ // Establishes the D-05 target matrix + JVM toolchain + common deps.
+ // Android bytecode is JVM 11 (D-08); server + desktop + shared/jvm are JVM 21.
+
+ import org.gradle.api.artifacts.VersionCatalogsExtension
+ import org.gradle.kotlin.dsl.getByType
+ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+ plugins {
+ id("org.jetbrains.kotlin.multiplatform")
+ }
+
+ val libs = extensions.getByType().named("libs")
+
+ kotlin {
+ jvmToolchain(21)
+
+ androidTarget {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
+ iosTarget.binaries.framework {
+ baseName = "ComposeApp"
+ isStatic = true
+ }
+ }
+
+ jvm {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
+ }
+
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs { browser() }
+
+ compilerOptions {
+ allWarningsAsErrors.set(true)
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(project.dependencies.platform(libs.findLibrary("koin-bom").get()))
+ implementation(libs.findLibrary("koin-core").get())
+ implementation(libs.findLibrary("kermit").get())
+ }
+ commonTest.dependencies {
+ implementation(libs.findLibrary("kotlin-test").get())
+ }
+ }
+ }
+ ```
+
+ ---
+
+ **File 5: `build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts`** (01-RESEARCH.md lines 447-477 + 01-PATTERNS.md lines 247-287 — layers on KMP, PITFALL #2):
+
+ ```kotlin
+ import org.gradle.api.artifacts.VersionCatalogsExtension
+ import org.gradle.kotlin.dsl.getByType
+
+ plugins {
+ id("recipe.kotlin.multiplatform")
+ id("org.jetbrains.compose")
+ id("org.jetbrains.kotlin.plugin.compose")
+ id("org.jetbrains.compose.hot-reload")
+ }
+
+ val libs = extensions.getByType().named("libs")
+
+ kotlin {
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.findLibrary("compose-runtime").get())
+ implementation(libs.findLibrary("compose-foundation").get())
+ implementation(libs.findLibrary("compose-material3").get())
+ implementation(libs.findLibrary("compose-ui").get())
+ implementation(libs.findLibrary("compose-components-resources").get())
+ implementation(libs.findLibrary("androidx-lifecycle-viewmodelCompose").get())
+ implementation(libs.findLibrary("androidx-lifecycle-runtimeCompose").get())
+ implementation(libs.findLibrary("koin-compose").get())
+ implementation(libs.findLibrary("koin-composeViewmodel").get())
+ }
+ }
+ }
+ ```
+
+ CRITICAL: this plugin applies `id("recipe.kotlin.multiplatform")` — NOT `id("org.jetbrains.kotlin.multiplatform")`. The KMP plugin is applied transitively by the recipe plugin. Double-applying throws "Plugin already applied" (PITFALL #2).
+
+ ---
+
+ **File 6: `build-logic/src/main/kotlin/recipe.android.application.gradle.kts`** (01-RESEARCH.md lines 516-552, catalog-accessor-adjusted for precompiled-plugin context):
+
+ ```kotlin
+ import org.gradle.api.artifacts.VersionCatalogsExtension
+ import org.gradle.kotlin.dsl.getByType
+
+ plugins {
+ id("com.android.application")
+ }
+
+ val libs = extensions.getByType().named("libs")
+
+ android {
+ namespace = "dev.ulfrx.recipe"
+ compileSdk = libs.findVersion("android-compileSdk").get().toString().toInt()
+
+ defaultConfig {
+ applicationId = "dev.ulfrx.recipe"
+ minSdk = libs.findVersion("android-minSdk").get().toString().toInt()
+ targetSdk = libs.findVersion("android-targetSdk").get().toString().toInt()
+ versionCode = 1
+ versionName = "1.0"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ }
+ ```
+
+ CRITICAL: the version lookup is `libs.findVersion("android-compileSdk").get().toString().toInt()` — NOT `libs.versions.android.compileSdk.get().toInt()` (that accessor does not exist in precompiled plugins — PITFALL #1).
+
+ ---
+
+ **File 7: `build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts`** (01-RESEARCH.md lines 558-601, quoted-config variant per PATTERNS.md line 395):
+
+ ```kotlin
+ import org.gradle.api.artifacts.VersionCatalogsExtension
+ import org.gradle.kotlin.dsl.getByType
+
+ plugins {
+ id("org.jetbrains.kotlin.jvm")
+ id("io.ktor.plugin")
+ id("org.flywaydb.flyway")
+ application
+ }
+
+ val libs = extensions.getByType().named("libs")
+
+ kotlin {
+ jvmToolchain(21)
+ compilerOptions {
+ allWarningsAsErrors.set(true)
+ }
+ }
+
+ dependencies {
+ "implementation"(libs.findLibrary("ktor-serverCore").get())
+ "implementation"(libs.findLibrary("ktor-serverNetty").get())
+ "implementation"(libs.findLibrary("ktor-serverContentNegotiation").get())
+ "implementation"(libs.findLibrary("ktor-serializationKotlinxJson").get())
+ "implementation"(libs.findLibrary("logback").get())
+ "implementation"(libs.findLibrary("flyway-core").get())
+ "implementation"(libs.findLibrary("flyway-database-postgresql").get())
+ "implementation"(libs.findLibrary("postgresql").get())
+ "testImplementation"(libs.findLibrary("ktor-serverTestHost").get())
+ "testImplementation"(libs.findLibrary("kotlin-testJunit").get())
+ }
+
+ flyway {
+ url = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/recipe"
+ user = System.getenv("DATABASE_USER") ?: "recipe"
+ password = System.getenv("DATABASE_PASSWORD") ?: "recipe"
+ locations = arrayOf("classpath:db/migration")
+ cleanDisabled = true
+ baselineOnMigrate = true
+ validateOnMigrate = true
+ }
+ ```
+
+ CRITICAL:
+ - `"implementation"(...)` with quoted-string configuration is MANDATORY inside precompiled plugins — the unquoted form is a typed method that only exists in module build scripts.
+ - The `flyway { }` block is for CLI ergonomics (`./gradlew flywayInfo`). Runtime migration uses the Java API (Plan 05 wires this).
+
+ ---
+
+ After writing all 7 files, verify that `build-logic/build.gradle.kts` can see the catalog by running a syntax-only check. No `./gradlew build` yet — Plan 03 wires the modules.
+
+
+ test -f build-logic/settings.gradle.kts && test -f build-logic/build.gradle.kts && test -f build-logic/src/main/kotlin/recipe.quality.gradle.kts && test -f build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts && test -f build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts && test -f build-logic/src/main/kotlin/recipe.android.application.gradle.kts && test -f build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts && grep -q 'from(files("../gradle/libs.versions.toml"))' build-logic/settings.gradle.kts && grep -q '`kotlin-dsl`' build-logic/build.gradle.kts && grep -q 'asDependency' build-logic/build.gradle.kts && grep -q 'id("org.jetbrains.kotlin.multiplatform")' build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts && grep -q 'id("recipe.kotlin.multiplatform")' build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts && ! grep -q 'id("org.jetbrains.kotlin.multiplatform")' build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts && grep -q '"implementation"' build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts && grep -q 'extensions.getByType' build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts
+
+
+ - All 7 files exist at their declared paths
+ - `build-logic/settings.gradle.kts` contains literal `from(files("../gradle/libs.versions.toml"))`
+ - `build-logic/settings.gradle.kts` ends with `rootProject.name = "build-logic"`
+ - `build-logic/build.gradle.kts` contains `` `kotlin-dsl` `` (triple-backtick plugin alias)
+ - `build-logic/build.gradle.kts` defines the `Provider.asDependency()` extension function
+ - `build-logic/build.gradle.kts` has exactly 9 `compileOnly(libs.plugins.*.asDependency())` calls (kotlinMultiplatform, androidApplication, composeMultiplatform, composeCompiler, composeHotReload, kotlinJvm, ktor, spotless, flywayPlugin) — no `androidLibrary` because no precompiled plugin applies `com.android.library`; `shared/build.gradle.kts` applies that alias directly
+ - `recipe.kotlin.multiplatform.gradle.kts` contains `id("org.jetbrains.kotlin.multiplatform")` (exactly ONCE, in the plugins block)
+ - `recipe.kotlin.multiplatform.gradle.kts` contains `baseName = "ComposeApp"` (D-20 / PITFALL #10)
+ - `recipe.kotlin.multiplatform.gradle.kts` contains `jvmToolchain(21)` AND `JvmTarget.JVM_11` AND `JvmTarget.JVM_21` (D-08 split)
+ - `recipe.kotlin.multiplatform.gradle.kts` contains `allWarningsAsErrors.set(true)` at the `kotlin { compilerOptions { } }` extension level (D-11)
+ - `recipe.kotlin.multiplatform.gradle.kts` does NOT contain `js {` or `iosX64` (D-01 / D-02)
+ - `recipe.compose.multiplatform.gradle.kts` contains `id("recipe.kotlin.multiplatform")` AND does NOT contain `id("org.jetbrains.kotlin.multiplatform")` (PITFALL #2 guard)
+ - `recipe.compose.multiplatform.gradle.kts` contains `id("org.jetbrains.compose.hot-reload")` (preserves commit c50d747)
+ - `recipe.android.application.gradle.kts` contains `namespace = "dev.ulfrx.recipe"` (D-20)
+ - `recipe.android.application.gradle.kts` uses `libs.findVersion("android-compileSdk").get().toString().toInt()` (PITFALL #1)
+ - `recipe.jvm.server.gradle.kts` uses quoted `"implementation"` (not unquoted `implementation(...)` — quoted-config footgun)
+ - `recipe.jvm.server.gradle.kts` contains `cleanDisabled = true` (PITFALL #6 safety)
+ - `recipe.quality.gradle.kts` contains `targetExclude("**/build/**", "**/generated/**")` (avoids scanning generated Compose resources)
+ - Every precompiled plugin that reads the catalog contains `extensions.getByType().named("libs")`
+
+ build-logic/ scaffold complete; all 7 files follow canonical patterns; no PITFALL #1/#2/#7/#9/#10 violations detectable via grep.
+
+
+
+ Task 2: Wire build-logic into root settings.gradle.kts and update root build.gradle.kts
+ settings.gradle.kts, build.gradle.kts
+
+ - settings.gradle.kts (current 37-line content — target of edit)
+ - build.gradle.kts (current 12-line content — target of edit)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 749-767 (PITFALL #9 — includeBuild MUST be inside pluginManagement)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 510-572 (settings.gradle.kts + root build.gradle.kts deltas)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md lines 107-109 (build-logic/ as included build — standard Gradle pattern)
+
+
+ Edit two files.
+
+ ---
+
+ **Edit 1: `settings.gradle.kts`** — add `includeBuild("build-logic")` as the FIRST statement inside the existing `pluginManagement { }` block. Do NOT move or remove any other line.
+
+ The current `pluginManagement { }` block (lines 4-16 of the existing file) should become:
+
+ ```kotlin
+ pluginManagement {
+ includeBuild("build-logic")
+ repositories {
+ google {
+ mavenContent {
+ includeGroupAndSubgroups("androidx")
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+ }
+ ```
+
+ PITFALL #9 is load-bearing: `includeBuild` MUST be inside `pluginManagement { }`, NOT at top level, and NOT inside `dependencyResolutionManagement { }`. Placing it elsewhere means child modules cannot resolve `id("recipe.*")` plugin IDs.
+
+ Do NOT modify:
+ - Line 1: `rootProject.name = "recipe"`
+ - Line 2: `enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")`
+ - `dependencyResolutionManagement { }` block
+ - `plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" }`
+ - `include(":composeApp")`, `include(":server")`, `include(":shared")`
+
+ ---
+
+ **Edit 2: `build.gradle.kts`** — append two new `alias(...) apply false` entries to the existing plugins block. Keep the existing 8 entries in their current order.
+
+ Result:
+
+ ```kotlin
+ plugins {
+ // this is necessary to avoid the plugins to be loaded multiple times
+ // in each subproject's classloader
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.androidLibrary) apply false
+ alias(libs.plugins.composeHotReload) apply false
+ alias(libs.plugins.composeMultiplatform) apply false
+ alias(libs.plugins.composeCompiler) apply false
+ alias(libs.plugins.kotlinJvm) apply false
+ alias(libs.plugins.kotlinMultiplatform) apply false
+ alias(libs.plugins.ktor) apply false
+ alias(libs.plugins.spotless) apply false
+ alias(libs.plugins.flywayPlugin) apply false
+ }
+ ```
+
+ Why the `apply false` entries: Gradle's plugin classloader uses these declarations as hints when the plugin is applied through an included-build's precompiled plugin. `recipe.quality` applies `com.diffplug.spotless` and `recipe.jvm.server` applies `org.flywaydb.flyway` — the root `apply false` entries ensure a single resolved classpath per plugin ID (per the existing template's comment).
+
+
+ grep -q 'includeBuild("build-logic")' settings.gradle.kts && awk '/pluginManagement \{/,/^\}/' settings.gradle.kts | grep -q 'includeBuild("build-logic")' && ! awk '/dependencyResolutionManagement \{/,/^\}/' settings.gradle.kts | grep -q 'includeBuild' && grep -q 'alias(libs.plugins.spotless) apply false' build.gradle.kts && grep -q 'alias(libs.plugins.flywayPlugin) apply false' build.gradle.kts && grep -c 'apply false' build.gradle.kts | grep -q '^10$'
+
+
+ - `settings.gradle.kts` contains `includeBuild("build-logic")` exactly 1 time
+ - That `includeBuild("build-logic")` line appears INSIDE the `pluginManagement { ... }` block (verifiable: `awk '/pluginManagement \{/,/^\}/' settings.gradle.kts | grep -q 'includeBuild("build-logic")'`)
+ - `settings.gradle.kts` does NOT contain `includeBuild` anywhere else (NOT at top level, NOT in `dependencyResolutionManagement`)
+ - `settings.gradle.kts` still contains `rootProject.name = "recipe"` (unmodified line 1)
+ - `settings.gradle.kts` still contains `include(":composeApp")`, `include(":server")`, `include(":shared")` (unmodified)
+ - `build.gradle.kts` contains `alias(libs.plugins.spotless) apply false`
+ - `build.gradle.kts` contains `alias(libs.plugins.flywayPlugin) apply false`
+ - `grep -c 'apply false' build.gradle.kts` returns `10` (8 existing + 2 new)
+ - All 8 existing `alias(...)` lines are preserved
+
+ build-logic/ is discoverable as an included build for plugin resolution; root `build.gradle.kts` declares classloader hints for Spotless + Flyway.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Gradle build → build-logic/ (included build) | Same-repo; no external trust boundary. Precompiled plugins run in the Gradle daemon's JVM with full project access by design. |
+| build-logic precompiled plugins → Maven Central + plugin portal | Inherits repository set from `build-logic/settings.gradle.kts.dependencyResolutionManagement` (google, mavenCentral, gradlePluginPortal). Pinned plugin versions via catalog aliases. |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-02-01 | Tampering (supply chain) | Precompiled plugin classpath | mitigate | Plugin versions resolved exclusively from catalog aliases via `asDependency()` — no literal versions leak into build-logic/build.gradle.kts. D-09 catalog-only rule enforced by Plan 07's `tools/verify-no-version-literals.sh`. |
+| T-01-02-02 | Elevation of Privilege | `recipe.jvm.server` applying Flyway to non-server modules | mitigate | `recipe.jvm.server` is applied ONLY to `server/build.gradle.kts` (Plan 03). The plugin bundles `io.ktor.plugin` + `org.flywaydb.flyway` + Postgres JDBC — if accidentally applied to `composeApp`, AGP would fail at configuration time. Role-declaration design (D-06) makes misuse obvious. |
+| T-01-02-03 | Tampering | `recipe.quality` Spotless scanning untrusted paths | accept | Spotless config restricted via `target("src/**/*.kt")` + `targetExclude("**/build/**", "**/generated/**")`. No execution of scanned code; ktlint is pure static analysis. |
+| T-01-02-04 | Denial of Service | Misspelled plugin ID breaks entire root build | mitigate | Task 1 `` greps for exact plugin IDs and the `id("recipe.kotlin.multiplatform")` layering in `recipe.compose.multiplatform.gradle.kts`. Plan 03's `./gradlew help` invocations will surface any remaining typos immediately. |
+
+
+
+Phase-level verification for this plan:
+
+- `tools/verify-no-version-literals.sh` still exits 0 (build-logic/build.gradle.kts is explicitly excluded by the script — the `asDependency()` coordinates contain a version string as part of the synthesized artifact coord, but the script excludes that single file).
+- No Gradle command is run yet — Plan 03 refactors modules to apply these plugins; until then, the root `./gradlew build` will still work against the EXISTING module build files (which have not yet been refactored).
+
+Optional fast sanity check (if needed):
+- `./gradlew --help` exits 0 (proves `settings.gradle.kts` still parses).
+- `./gradlew help` (without args) exits 0 (proves `includeBuild` is legal).
+
+These sanity checks are NOT in the `` verify blocks to keep them fast; run them once manually if a later plan fails unexpectedly.
+
+
+
+- 7 files under `build-logic/` created with canonical content (exact path listing in `files_modified`)
+- `settings.gradle.kts` has `includeBuild("build-logic")` inside `pluginManagement { }`
+- `build.gradle.kts` has 10 `apply false` entries (8 existing + 2 new for Spotless + Flyway)
+- No existing version aliases or source files modified in Plan 01 or prior
+- `tools/verify-no-version-literals.sh` continues to exit 0
+
+
+
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-02-SUMMARY.md b/.planning/phases/01-project-infrastructure-module-wiring/01-02-SUMMARY.md
new file mode 100644
index 0000000..8da32b3
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-02-SUMMARY.md
@@ -0,0 +1,98 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 02
+subsystem: infra
+tags: [gradle, build-logic, included-build, precompiled-plugins, version-catalog, kotlin-multiplatform, compose, ktor, spotless, flyway, pitfall-1, pitfall-2, pitfall-9, pitfall-10]
+
+requires: [01-01]
+provides:
+ - "build-logic/ included build resolving the parent catalog via files(\"../gradle/libs.versions.toml\")"
+ - "Precompiled plugin recipe.quality (Spotless + ktlint + D-11 allWarningsAsErrors safety net via plugins.withId guard)"
+ - "Precompiled plugin recipe.kotlin.multiplatform (D-05 target matrix: androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs; JVM toolchain 21 + JVM 11 Android bytecode per D-08; framework baseName = ComposeApp; Koin BOM + koin-core + Kermit + kotlin-test common deps; allWarningsAsErrors at kotlin{} level)"
+ - "Precompiled plugin recipe.compose.multiplatform (layers on recipe.kotlin.multiplatform — PITFALL #2 avoided; Compose + composeCompiler + composeHotReload + commonMain Compose deps + lifecycle-viewmodel-compose + koin-compose)"
+ - "Precompiled plugin recipe.android.application (namespace dev.ulfrx.recipe; findVersion catalog accessor per PITFALL #1; SDK versions from catalog)"
+ - "Precompiled plugin recipe.jvm.server (Kotlin JVM + Ktor + Flyway + application; quoted \"implementation\" configs; cleanDisabled=true; D-08 JVM toolchain 21)"
+ - "Root settings.gradle.kts with includeBuild(\"build-logic\") placed inside pluginManagement{} (PITFALL #9)"
+ - "Root build.gradle.kts with 10 alias(...) apply false entries (8 existing + Spotless + Flyway classloader hints)"
+affects: [01-03, 01-04, 01-05, 01-06, 01-07]
+
+tech-stack:
+ added:
+ - "build-logic/ included build (kotlin-dsl convention-plugin project)"
+ - "5 precompiled script plugins: recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server"
+ patterns:
+ - "PITFALL #1 mitigation: every precompiled plugin reads versions via extensions.getByType().named(\"libs\")"
+ - "PITFALL #2 mitigation: recipe.compose.multiplatform applies id(\"recipe.kotlin.multiplatform\") — KMP plugin applied transitively"
+ - "PITFALL #9 mitigation: includeBuild(\"build-logic\") sits inside pluginManagement{}"
+ - "PITFALL #10 mitigation: baseName = \"ComposeApp\" set on both iOS frameworks"
+ - "Quoted-configuration footgun avoidance: recipe.jvm.server uses \"implementation\"(...) string-literal configs"
+ - "D-11 redundancy guard: recipe.quality uses plugins.withId guards for composability"
+ - "Plugin coordinate synthesis via Provider.asDependency() keeps build-logic/build.gradle.kts catalog-only"
+
+key-files:
+ created:
+ - build-logic/settings.gradle.kts
+ - build-logic/build.gradle.kts
+ - build-logic/src/main/kotlin/recipe.quality.gradle.kts
+ - build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts
+ - build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts
+ - build-logic/src/main/kotlin/recipe.android.application.gradle.kts
+ - build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
+ modified:
+ - settings.gradle.kts
+ - build.gradle.kts
+
+key-decisions:
+ - "Content for all 7 build-logic/ files copied verbatim from 01-RESEARCH.md § Code Examples + 01-PATTERNS.md; no structural changes."
+ - "9 compileOnly(...asDependency()) entries omit androidLibrary — no recipe-family precompiled plugin applies com.android.library; shared/build.gradle.kts applies that plugin directly in 01-03."
+ - "recipe.quality's D-11 safety net is plugins.withId-guarded so the plugin remains composable when applied standalone."
+
+patterns-established:
+ - "Pattern 1 (role declarations): each recipe.* plugin encodes a module role; shared/ cannot pull Compose transitively (INFRA-06)."
+ - "Pattern 2 (catalog-only versioning inside build-logic): plugin coordinates via asDependency(); library refs via findLibrary; version refs via findVersion.toString().toInt()."
+ - "Pattern 3 (Flyway CLI + runtime split): flyway{} block for CLI ergonomics; runtime migration handled in 01-05."
+ - "Pattern 4 (JVM target split): jvmToolchain(21) drives shared/server/desktop; Android bytecode pinned at JVM 11; server JVM output at JVM 21."
+
+requirements-completed: [INFRA-02]
+
+duration: ~5min
+completed: 2026-04-24
+tasks-completed: 2
+files-created: 7
+files-modified: 2
+---
+
+# Phase 01 Plan 02: build-logic included build + 5 precompiled script plugins
+
+`build-logic/` scaffolded as an included build whose 5 precompiled script plugins encode D-05/D-06/D-08/D-11/D-20 constraints once, and whose single hook into the root project is `includeBuild("build-logic")` inside `settings.gradle.kts pluginManagement { }` per PITFALL #9.
+
+## What was built
+
+- **`build-logic/settings.gradle.kts`** — resolves parent catalog via `from(files("../gradle/libs.versions.toml"))`; `rootProject.name = "build-logic"`.
+- **`build-logic/build.gradle.kts`** — applies `` `kotlin-dsl` ``; 9 `compileOnly(libs.plugins.*.asDependency())` entries; `Provider.asDependency()` extension synthesises coordinates.
+- **`recipe.quality.gradle.kts`** — Spotless + ktlint on `src/**/*.kt` with `targetExclude("**/build/**", "**/generated/**")`; two `plugins.withId` guards enforce `allWarningsAsErrors.set(true)` on `KotlinCompilationTask<*>` when a Kotlin plugin is present.
+- **`recipe.kotlin.multiplatform.gradle.kts`** — canonical KMP plugin; `jvmToolchain(21)`; `androidTarget { jvmTarget = JVM_11 }`; `iosArm64()` + `iosSimulatorArm64()` with `baseName = "ComposeApp"; isStatic = true`; `jvm { jvmTarget = JVM_21 }`; `wasmJs { browser() }`; commonMain deps: Koin BOM + koin-core + Kermit; commonTest: kotlin-test.
+- **`recipe.compose.multiplatform.gradle.kts`** — applies `id("recipe.kotlin.multiplatform")` (PITFALL #2) + compose MP + compose compiler + compose hot-reload; commonMain Compose deps + lifecycle-viewmodel + koin-compose.
+- **`recipe.android.application.gradle.kts`** — `namespace = "dev.ulfrx.recipe"`; SDK versions via `libs.findVersion("android-compileSdk").get().toString().toInt()` (PITFALL #1); JVM 11 compile options.
+- **`recipe.jvm.server.gradle.kts`** — Kotlin JVM + Ktor + Flyway + application; `jvmToolchain(21)` + `allWarningsAsErrors.set(true)`; 10 quoted `"implementation"(...)` deps (ktor-server*, logback, flyway-core + flyway-database-postgresql, postgresql JDBC, ktor-serverTestHost, kotlin-testJunit); `flyway{}` block with env-driven URL + `cleanDisabled = true`.
+- **Root `settings.gradle.kts`** — added `includeBuild("build-logic")` as first statement inside existing `pluginManagement { }`.
+- **Root `build.gradle.kts`** — appended `alias(libs.plugins.spotless) apply false` and `alias(libs.plugins.flywayPlugin) apply false`. Total apply-false count: 10.
+
+## Commits
+
+| Task | Description | Hash |
+|------|-------------|------|
+| 1 | Scaffold build-logic/ included build + 5 precompiled plugins | `6a69910` |
+| 2 | Wire build-logic into root settings.gradle.kts + Spotless/Flyway apply-false | `60221f6` |
+
+## Deviations from Plan
+
+None — every `` grep block and acceptance criterion passed first-try.
+
+## Note
+
+This SUMMARY.md was drafted by the executor agent but hook-blocked from being written inside the worktree sandbox; the orchestrator persisted it after merging the worktree into master.
+
+## Requirements completed
+
+INFRA-02
\ No newline at end of file
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-03-PLAN.md b/.planning/phases/01-project-infrastructure-module-wiring/01-03-PLAN.md
new file mode 100644
index 0000000..1377b87
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-03-PLAN.md
@@ -0,0 +1,352 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 03
+type: execute
+wave: 2
+depends_on: [01, 02]
+files_modified:
+ - composeApp/build.gradle.kts
+ - shared/build.gradle.kts
+ - server/build.gradle.kts
+ - shared/src/jsMain
+autonomous: true
+requirements: [INFRA-02, INFRA-06]
+requirements_addressed: [INFRA-02, INFRA-06]
+
+must_haves:
+ truths:
+ - "composeApp/build.gradle.kts applies recipe.kotlin.multiplatform + recipe.compose.multiplatform + recipe.android.application + recipe.quality, and nothing else (D-06 role declaration)"
+ - "shared/build.gradle.kts applies recipe.kotlin.multiplatform + recipe.quality + androidLibrary alias, with explicitApi() set directly in the module (D-12)"
+ - "server/build.gradle.kts applies recipe.jvm.server + recipe.quality, and keeps only the module-specific application { } block"
+ - "The js target is removed from composeApp and shared (D-01); shared/src/jsMain/ directory is deleted"
+ - "iosX64 target is never referenced (D-02) — only iosArm64 + iosSimulatorArm64 via the convention plugin"
+ - "No version literals exist in any *.gradle.kts outside gradle/libs.versions.toml (INFRA-01 / D-09)"
+ - "shared/ framework basename is overridden to 'Shared' (D-07, PITFALL #10); composeApp keeps 'ComposeApp' from the convention plugin"
+ artifacts:
+ - path: "composeApp/build.gradle.kts"
+ provides: "Module build applying 4 recipe.* convention plugins + module-only source-set deps (androidMain, commonMain projects.shared, jvmMain desktop)"
+ min_lines: 15
+ - path: "shared/build.gradle.kts"
+ provides: "Module build applying recipe.kotlin.multiplatform + recipe.quality + androidLibrary; enabling explicitApi(); overriding framework baseName to 'Shared'; keeping android { namespace } block"
+ min_lines: 15
+ - path: "server/build.gradle.kts"
+ provides: "Module build applying recipe.jvm.server + recipe.quality; keeping application { mainClass } block and implementation(projects.shared) dep"
+ min_lines: 10
+ key_links:
+ - from: "composeApp/build.gradle.kts"
+ to: "build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts"
+ via: "plugins { id(\"recipe.kotlin.multiplatform\") }"
+ pattern: "id\\(\"recipe\\.kotlin\\.multiplatform\"\\)"
+ - from: "composeApp/build.gradle.kts"
+ to: "build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts"
+ via: "plugins { id(\"recipe.compose.multiplatform\") }"
+ pattern: "id\\(\"recipe\\.compose\\.multiplatform\"\\)"
+ - from: "server/build.gradle.kts"
+ to: "build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts"
+ via: "plugins { id(\"recipe.jvm.server\") }"
+ pattern: "id\\(\"recipe\\.jvm\\.server\"\\)"
+---
+
+
+Refactor the three module build scripts (`composeApp/`, `shared/`, `server/`) to apply the convention plugins from Plan 02 and remove the content those plugins now own. Drop the `js` target (D-01), confirm `iosX64` stays absent (D-02), add `explicitApi()` + framework-basename override to `shared/` (D-12 / PITFALL #10), and ensure every module's `plugins { }` block reads as a role declaration (D-06). Also delete the `shared/src/jsMain/` source directory (D-01).
+
+Purpose: This plan delivers INFRA-02's structural payoff — adding a new KMP module in the future should require only `plugins { id("recipe.kotlin.multiplatform") }` + source-set declarations, not copy-pasting Compose configs. It also delivers INFRA-06's structural prerequisite: after this refactor, `shared/` no longer pulls Compose transitively (because `recipe.compose.multiplatform` is applied only to `composeApp/`).
+
+Output: Three rewritten `build.gradle.kts` files (each ≤40 lines), `shared/src/jsMain/` directory deleted. No `./gradlew build` run in this plan — Plan 04/05 verify via their own targets, Plan 07 runs the full green-build gate.
+
+
+
+@$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/build.gradle.kts
+@shared/build.gradle.kts
+@server/build.gradle.kts
+@CLAUDE.md
+
+
+
+
+From gradle/libs.versions.toml (Plan 01 extended):
+- `libs.plugins.androidLibrary` — still referenced as alias inside shared/build.gradle.kts
+- `libs.compose.uiToolingPreview` — referenced from composeApp/build.gradle.kts module-specific deps
+- `libs.androidx.activity.compose` — referenced from composeApp androidMain deps
+- `libs.kotlinx.coroutinesSwing` — referenced from composeApp jvmMain deps
+- `libs.compose.uiTooling` — referenced from composeApp debugImplementation
+- `libs.koin.android` — NEW alias (Plan 01) for MainApplication's `androidContext(...)` in Plan 04
+
+From build-logic/src/main/kotlin/ (Plan 02 created):
+- `recipe.kotlin.multiplatform` — applies KMP, sets D-05 targets, JVM toolchain, adds koin-bom/koin-core/kermit to commonMain, kotlin-test to commonTest, allWarningsAsErrors
+- `recipe.compose.multiplatform` — applies Compose MP + hot-reload on top of KMP, adds compose-* deps to commonMain
+- `recipe.android.application` — applies com.android.application, sets namespace + SDK versions
+- `recipe.jvm.server` — applies kotlin(jvm) + io.ktor.plugin + flyway + all server deps + quoted-config dependency block
+- `recipe.quality` — applies Spotless + allWarningsAsErrors safety net
+
+
+
+
+
+
+ Task 1: Rewrite composeApp/build.gradle.kts and shared/build.gradle.kts, delete shared/src/jsMain/
+ composeApp/build.gradle.kts, shared/build.gradle.kts, shared/src/jsMain
+
+ - composeApp/build.gradle.kts (current 114 lines — target of rewrite)
+ - shared/build.gradle.kts (current 55 lines — target of rewrite)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 574-672 (exact deltas for composeApp + shared)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-01 (drop js), D-03 (no desktop packaging), D-12 (explicitApi on shared only), D-20 (namespace + baseName)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1144-1155 (Open Question #1 — keep com.android.library on shared/ in Phase 1)
+
+
+ Two file rewrites plus one directory deletion.
+
+ **Rewrite 1: `composeApp/build.gradle.kts`** — replace entire file content with:
+
+ ```kotlin
+ plugins {
+ id("recipe.kotlin.multiplatform")
+ id("recipe.compose.multiplatform")
+ id("recipe.android.application")
+ id("recipe.quality")
+ }
+
+ kotlin {
+ sourceSets {
+ androidMain.dependencies {
+ implementation(libs.compose.uiToolingPreview)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.koin.android)
+ }
+ commonMain.dependencies {
+ implementation(libs.compose.uiToolingPreview)
+ implementation(projects.shared)
+ }
+ jvmMain.dependencies {
+ implementation(compose.desktop.currentOs)
+ implementation(libs.kotlinx.coroutinesSwing)
+ }
+ }
+ }
+
+ dependencies {
+ debugImplementation(libs.compose.uiTooling)
+ }
+ ```
+
+ DELETIONS relative to the current file:
+ - DROP all 3 imports on lines 1-3 (no longer needed — convention plugins supply JvmTarget/ExperimentalWasmDsl/TargetFormat)
+ - DROP the original `plugins { alias(...) alias(...) }` block (lines 5-11) — replaced with 4 recipe.* IDs
+ - DROP the `kotlin { androidTarget { ... } iosArm64() iosSimulatorArm64() jvm { ... } js { ... } wasmJs { ... } }` structural block (lines 13-46) — moved into `recipe.kotlin.multiplatform`
+ - DROP `commonMain.dependencies` Compose entries (lines 52-62 — compose.runtime, compose.foundation, compose.material3, compose.ui, compose.components.resources, androidx.lifecycle.viewmodelCompose, androidx.lifecycle.runtimeCompose) — moved into `recipe.compose.multiplatform`. KEEP `implementation(projects.shared)` and the module-only `implementation(libs.compose.uiToolingPreview)` (the preview tooling is needed by `@Preview` annotations in composeApp's common code).
+ - DROP `commonTest.dependencies { implementation(libs.kotlin.test) }` (lines 63-65) — moved into `recipe.kotlin.multiplatform`
+ - DROP the entire `android { ... }` block (lines 73-98) — moved into `recipe.android.application`
+ - DROP `compose.desktop { application { ... nativeDistributions { ... } } }` (lines 104-114) — D-03 says no desktop packaging
+
+ ADDITIONS:
+ - ADD `implementation(libs.koin.android)` to `androidMain.dependencies` (Plan 04's MainApplication.kt calls `androidContext(...)` which comes from koin-android; the catalog alias was added in Plan 01).
+
+ KEEP:
+ - `androidMain.dependencies { implementation(libs.compose.uiToolingPreview); implementation(libs.androidx.activity.compose) }` — Android-only deps
+ - `jvmMain.dependencies { implementation(compose.desktop.currentOs); implementation(libs.kotlinx.coroutinesSwing) }` — Desktop-only deps
+ - `dependencies { debugImplementation(libs.compose.uiTooling) }` — Android debug-only tooling
+
+ **Rewrite 2: `shared/build.gradle.kts`** — replace entire file content with:
+
+ ```kotlin
+ plugins {
+ id("recipe.kotlin.multiplatform")
+ id("recipe.quality")
+ alias(libs.plugins.androidLibrary)
+ }
+
+ kotlin {
+ explicitApi()
+
+ // Override framework baseName: shared exposes "Shared.framework" to Swift, while
+ // composeApp's convention-plugin default is "ComposeApp.framework". (D-07 / PITFALL #10)
+ targets.withType().configureEach {
+ binaries.withType().configureEach {
+ baseName = "Shared"
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ // Phase 1: intentionally empty. Domain models + DTOs land Phase 2+.
+ // D-19 / INFRA-06: Do NOT add Ktor, Compose, or SQLDelight deps here — EVER.
+ }
+ }
+ }
+
+ android {
+ namespace = "dev.ulfrx.recipe.shared"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ defaultConfig {
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ }
+ }
+ ```
+
+ DELETIONS relative to the current file:
+ - DROP both imports on lines 1-2 (no longer needed)
+ - DROP the original `plugins { alias(libs.plugins.kotlinMultiplatform); alias(libs.plugins.androidLibrary) }` (lines 4-7) — replaced with `id("recipe.kotlin.multiplatform")` + kept `alias(libs.plugins.androidLibrary)`
+ - DROP the entire `kotlin { androidTarget { ... } iosArm64() iosSimulatorArm64() jvm { ... } js { ... } wasmJs { ... } ... }` structural block (lines 9-41) — moved into `recipe.kotlin.multiplatform`
+ - DROP `js { browser() }` (lines 25-27) — D-01
+
+ ADDITIONS:
+ - ADD `explicitApi()` inside the `kotlin { }` block (D-12 — strict on shared/ only, configured directly in module)
+ - ADD the framework baseName override block targeting `KotlinNativeTarget`/`Framework` (overrides the convention plugin's `"ComposeApp"` default to `"Shared"` — D-07 / PITFALL #10)
+
+ KEEP:
+ - `android { namespace = "dev.ulfrx.recipe.shared"; compileSdk; compileOptions; defaultConfig.minSdk }` — per 01-RESEARCH.md Open Question #1, keep `com.android.library` applied in Phase 1 (deferring the "do we need it" question to a future `recipe.android.library` plugin)
+
+ Note on `libs.versions.android.compileSdk.get().toInt()` vs `libs.findVersion(...)`: the `libs.versions.*` accessor IS available in MODULE `build.gradle.kts` files (it's only unavailable in precompiled plugins — PITFALL #1 applies only there). So the typed accessor is correct here.
+
+ **Directory deletion: `shared/src/jsMain/`**
+
+ Delete the entire `shared/src/jsMain/` directory (contains `kotlin/dev/ulfrx/recipe/Platform.js.kt`). D-01 drops the `js` target; with `recipe.kotlin.multiplatform` no longer declaring `js()`, this source directory becomes orphaned.
+
+ Run: `rm -rf shared/src/jsMain`
+
+ Do NOT delete `shared/src/wasmJsMain/` — `wasmJs` is kept per D-01. `composeApp/src/webMain/` is the wasmJs source set, also kept.
+
+
+ grep -q 'id("recipe.kotlin.multiplatform")' composeApp/build.gradle.kts && grep -q 'id("recipe.compose.multiplatform")' composeApp/build.gradle.kts && grep -q 'id("recipe.android.application")' composeApp/build.gradle.kts && grep -q 'id("recipe.quality")' composeApp/build.gradle.kts && ! grep -q 'androidTarget' composeApp/build.gradle.kts && ! grep -q 'iosArm64' composeApp/build.gradle.kts && ! grep -q 'js {' composeApp/build.gradle.kts && ! grep -q 'nativeDistributions' composeApp/build.gradle.kts && ! grep -q '^android {' composeApp/build.gradle.kts && grep -q 'implementation(libs.koin.android)' composeApp/build.gradle.kts && grep -q 'id("recipe.kotlin.multiplatform")' shared/build.gradle.kts && grep -q 'id("recipe.quality")' shared/build.gradle.kts && grep -q 'explicitApi()' shared/build.gradle.kts && grep -q 'baseName = "Shared"' shared/build.gradle.kts && ! grep -q 'js {' shared/build.gradle.kts && ! test -d shared/src/jsMain && bash tools/verify-no-version-literals.sh && bash tools/verify-shared-pure.sh
+
+
+ - `composeApp/build.gradle.kts` has exactly 4 `id("recipe.*")` lines: `recipe.kotlin.multiplatform`, `recipe.compose.multiplatform`, `recipe.android.application`, `recipe.quality`
+ - `composeApp/build.gradle.kts` does NOT contain `androidTarget`, `iosArm64`, `iosSimulatorArm64`, `jvm {`, `js {`, `wasmJs {`, or any `binaries.framework` block (all moved to convention plugin)
+ - `composeApp/build.gradle.kts` does NOT contain an `^android {` block header (moved to `recipe.android.application`)
+ - `composeApp/build.gradle.kts` does NOT contain `nativeDistributions` or `compose.desktop { application { ... } }` (D-03)
+ - `composeApp/build.gradle.kts` does NOT contain `import org.jetbrains.compose.desktop.application.dsl.TargetFormat` (D-03)
+ - `composeApp/build.gradle.kts` contains `implementation(libs.koin.android)` inside an `androidMain.dependencies` block
+ - `composeApp/build.gradle.kts` contains `implementation(projects.shared)` in `commonMain.dependencies` (preserved for Plan 04 usage)
+ - `composeApp/build.gradle.kts` line count ≤ 30 (was 114)
+ - `shared/build.gradle.kts` has `id("recipe.kotlin.multiplatform")` + `id("recipe.quality")` + `alias(libs.plugins.androidLibrary)` (exactly 3 plugin applications)
+ - `shared/build.gradle.kts` contains `explicitApi()` (D-12)
+ - `shared/build.gradle.kts` contains `baseName = "Shared"` (exactly that capitalization — PITFALL #10)
+ - `shared/build.gradle.kts` does NOT contain `js {` or `iosX64`
+ - `shared/build.gradle.kts` contains the `android { namespace = "dev.ulfrx.recipe.shared" }` block (kept per Open Question #1)
+ - `shared/src/jsMain` directory no longer exists (`test ! -d shared/src/jsMain`)
+ - `tools/verify-no-version-literals.sh` exits 0 (no version literals leaked during rewrite)
+ - `tools/verify-shared-pure.sh` exits 0 (shared/commonMain has only Greeting.kt/Platform.kt/Constants.kt — no forbidden imports)
+
+ Both module builds apply recipe.* conventions; js target source dir deleted; explicitApi + Shared basename set on shared/.
+
+
+
+ Task 2: Rewrite server/build.gradle.kts
+ server/build.gradle.kts
+
+ - server/build.gradle.kts (current 23 lines — target of rewrite)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 674-706 (server/build.gradle.kts delta)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 556-605 (§ Pattern 7 — what's ALREADY in the convention plugin and does NOT need to be in the module)
+ - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-16 (server scope — Flyway, Postgres, /health)
+
+
+ Replace the entire content of `server/build.gradle.kts` with:
+
+ ```kotlin
+ plugins {
+ id("recipe.jvm.server")
+ id("recipe.quality")
+ }
+
+ group = "dev.ulfrx.recipe"
+ version = "1.0.0"
+
+ application {
+ mainClass.set("dev.ulfrx.recipe.ApplicationKt")
+
+ val isDevelopment: Boolean = project.ext.has("development")
+ applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
+ }
+
+ dependencies {
+ implementation(projects.shared)
+ }
+ ```
+
+ DELETIONS:
+ - DROP original plugins block (lines 1-5 — `alias(libs.plugins.kotlinJvm); alias(libs.plugins.ktor); application`) → replaced with 2 recipe.* IDs. The `application` plugin is applied by `recipe.jvm.server`.
+ - DROP individual dependency lines (lines 16-22 — `libs.logback`, `libs.ktor.serverCore`, `libs.ktor.serverNetty`, `libs.ktor.serverTestHost`, `libs.kotlin.testJunit`) → all moved into `recipe.jvm.server`.
+
+ KEEP:
+ - `group = "dev.ulfrx.recipe"` and `version = "1.0.0"` (module coordinates — per-module concern)
+ - `application { mainClass.set(...) }` + `applicationDefaultJvmArgs` (per-module config — the `application` plugin is applied by `recipe.jvm.server` but this config is module-specific)
+ - `implementation(projects.shared)` — module-specific project dependency (server depends on shared for Greeting, SERVER_PORT, future DTOs)
+
+ Note: `ktor-serverContentNegotiation`, `ktor-serializationKotlinxJson`, `flyway-core`, `flyway-database-postgresql`, `postgresql` are ALL bundled in `recipe.jvm.server` and do NOT need to be declared here.
+
+
+ grep -q 'id("recipe.jvm.server")' server/build.gradle.kts && grep -q 'id("recipe.quality")' server/build.gradle.kts && ! grep -q 'libs.plugins.kotlinJvm' server/build.gradle.kts && ! grep -q 'libs.plugins.ktor' server/build.gradle.kts && grep -q 'mainClass.set("dev.ulfrx.recipe.ApplicationKt")' server/build.gradle.kts && grep -q 'implementation(projects.shared)' server/build.gradle.kts && ! grep -q 'libs.logback' server/build.gradle.kts && ! grep -q 'libs.ktor.serverCore' server/build.gradle.kts && bash tools/verify-no-version-literals.sh
+
+
+ - `server/build.gradle.kts` has exactly 2 `id("recipe.*")` lines: `recipe.jvm.server`, `recipe.quality`
+ - `server/build.gradle.kts` does NOT contain `alias(libs.plugins.kotlinJvm)` or `alias(libs.plugins.ktor)`
+ - `server/build.gradle.kts` does NOT contain `libs.logback`, `libs.ktor.serverCore`, `libs.ktor.serverNetty`, `libs.ktor.serverTestHost`, or `libs.kotlin.testJunit` (all relocated to convention plugin)
+ - `server/build.gradle.kts` contains `mainClass.set("dev.ulfrx.recipe.ApplicationKt")` (unchanged)
+ - `server/build.gradle.kts` contains `implementation(projects.shared)` (unchanged)
+ - `server/build.gradle.kts` contains `group = "dev.ulfrx.recipe"` and `version = "1.0.0"` (unchanged module coordinates)
+ - `server/build.gradle.kts` line count ≤ 20 (was 23; effectively unchanged but deps block shrinks)
+ - `tools/verify-no-version-literals.sh` exits 0
+
+ server build applies recipe.jvm.server + recipe.quality; module-only config (mainClass, shared dep) preserved.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Module build scripts → build-logic precompiled plugins | Same repo; plugins apply privileged build configuration (namespace, SDK versions, dep injection). No external trust boundary. |
+| Gradle module configuration → dependency resolution | Same as Plan 02 — aliases resolved via `libs.versions.toml` (pinned); no runtime consequences until Plan 04/05 actually compile code. |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-03-01 | Tampering (supply-chain leak) | Accidental version literal in rewrites | mitigate | Every task's `` runs `tools/verify-no-version-literals.sh` which scans every `*.gradle.kts` after the rewrite. Any inlined version (e.g. a forgotten `"1.0.0"` as a dep version) fails the check. Note: `version = "1.0.0"` on `server/build.gradle.kts` line 2 is PROJECT coordinate, not a dependency version — the verify script targets `version\s*=\s*"[0-9]` inside dependency declarations only; project-version assignments pass (not declared as `libs.*` lookup). Verify script scope matches PATTERNS.md spec. |
+| T-01-03-02 | Elevation of Privilege | Compose deps leak into shared/ | mitigate | `shared/build.gradle.kts` applies ONLY `recipe.kotlin.multiplatform` + `recipe.quality` + `androidLibrary` — NOT `recipe.compose.multiplatform`. Plan 07's `tools/verify-shared-pure.sh` will catch forbidden imports if they ever appear. |
+| T-01-03-03 | Denial of Service | Missing `recipe.compose.multiplatform` application on composeApp breaks Compose | mitigate | Task 1 `` greps for all 4 recipe IDs explicitly. Plan 04 will fail at compile time if the Compose plugin ID is missing. |
+| T-01-03-04 | Tampering | `js` target remnants in source tree after D-01 drop | mitigate | Task 1 explicitly deletes `shared/src/jsMain/` directory and greps for `js {` blocks. `composeApp/src/webMain/` (wasmJs target, kept) is NOT touched. |
+
+
+
+Phase-level verification for this plan:
+
+- All three `tools/verify-*.sh` scripts exit 0 after rewrites.
+- `shared/src/jsMain/` directory no longer exists.
+- `composeApp/build.gradle.kts` shrinks from 114 to ~30 lines — INFRA-02 payoff visible.
+- `shared/build.gradle.kts` shrinks from 55 to ~35 lines and now sets `explicitApi()`.
+
+Optional sanity check (NOT in `` — Plan 07 runs the full gate):
+- `./gradlew :composeApp:help -q` emits a non-empty help output without a configuration error (proves plugin IDs resolve). Skip for speed — Plan 04 and Plan 05 will surface plugin-application errors via their own `./gradlew` targets.
+
+
+
+- `composeApp/build.gradle.kts` applies 4 recipe.* IDs and contains NO `kotlin { androidTarget { ... } ... }` structural block and NO `android { ... }` block and NO `nativeDistributions`
+- `shared/build.gradle.kts` applies 3 plugins (2 recipe.* + androidLibrary), enables `explicitApi()`, overrides baseName to `"Shared"`
+- `server/build.gradle.kts` applies 2 recipe.* IDs and keeps only `application { mainClass }` + `implementation(projects.shared)`
+- `shared/src/jsMain/` deleted
+- `tools/verify-no-version-literals.sh` exits 0
+- `tools/verify-shared-pure.sh` exits 0
+
+
+
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-03-SUMMARY.md b/.planning/phases/01-project-infrastructure-module-wiring/01-03-SUMMARY.md
new file mode 100644
index 0000000..614a19c
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-03-SUMMARY.md
@@ -0,0 +1,148 @@
+---
+phase: 01-project-infrastructure-module-wiring
+plan: 03
+subsystem: infra
+tags: [gradle, kmp, convention-plugins, compose-multiplatform, ktor-server, android-library, explicitApi, ios-framework]
+
+# Dependency graph
+requires:
+ - phase: 01-project-infrastructure-module-wiring
+ provides: "Plan 01 extended libs.versions.toml (koin.android alias); Plan 02 created 5 recipe.* convention plugins in build-logic/"
+provides:
+ - "composeApp/build.gradle.kts reduced from 114 to 28 lines; role-declaration plugin block applying recipe.kotlin.multiplatform + recipe.compose.multiplatform + recipe.android.application + recipe.quality"
+ - "shared/build.gradle.kts reduced from 55 to 36 lines; applies recipe.kotlin.multiplatform + recipe.quality + androidLibrary; enables explicitApi(); overrides iOS framework baseName to 'Shared'"
+ - "server/build.gradle.kts reduced from 23 to 18 lines; applies recipe.jvm.server + recipe.quality; retains only module-specific mainClass + projects.shared dep"
+ - "js target fully removed: shared/src/jsMain/ directory deleted (D-01)"
+ - "iosX64 remains absent across all modules (D-02)"
+ - "INFRA-02 structural payoff visible: adding a new KMP module henceforth requires only plugins { id('recipe.kotlin.multiplatform') } + sourceSet declarations"
+ - "INFRA-06 structural prerequisite: shared/ no longer applies recipe.compose.multiplatform, so Compose cannot leak transitively"
+affects: [02-auth, 03-households, 04-sync-skeleton, 05-recipe-catalog, 10-ui-chrome]
+
+# Tech tracking
+tech-stack:
+ added: [] # Plan 03 is pure refactor — all libraries/tools already added in Plans 01/02
+ patterns:
+ - "Role-declaration plugin blocks (D-06): module build.gradle.kts plugins {} lists only recipe.* IDs + module-specific aliases (e.g. androidLibrary on shared/)"
+ - "Per-module override pattern: shared/ overrides framework baseName by targeting KotlinNativeTarget + Framework directly in the module, not from the convention plugin (D-07 / PITFALL #10)"
+ - "Module-specific dep retention: jvmMain compose.desktop.currentOs + kotlinx.coroutinesSwing stay in composeApp; android debug-only libs.compose.uiTooling stays as debugImplementation"
+
+key-files:
+ created: []
+ modified:
+ - "composeApp/build.gradle.kts — rewritten: 4 recipe.* plugin IDs + 3-source-set dep block + 1 debug tooling line"
+ - "shared/build.gradle.kts — rewritten: 3 plugins + explicitApi() + Framework baseName override + android {} block retained"
+ - "server/build.gradle.kts — rewritten: 2 recipe.* plugin IDs + application {} + projects.shared dep"
+ - "shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt — DELETED (D-01 drops js target)"
+
+key-decisions:
+ - "Keep android { namespace = 'dev.ulfrx.recipe.shared' } block applied in Phase 1 per Open Question #1 (com.android.library retained; future recipe.android.library convention plugin deferred)"
+ - "libs.versions.* typed accessor used directly in module build.gradle.kts (not libs.findVersion) — PITFALL #1 only applies to precompiled plugin scripts, not module scripts"
+ - "libs.koin.android added to composeApp androidMain (not commonMain) — Koin's androidContext(...) lives in the android-specific artifact; commonMain stays platform-neutral"
+ - "Framework baseName override placed in the module, not hoisted into recipe.kotlin.multiplatform — shared/ is the only module needing 'Shared' (composeApp keeps convention default 'ComposeApp'), so keeping it local avoids a plugin parameter"
+
+patterns-established:
+ - "Plugin role declaration: each module build.gradle.kts opens with id('recipe.') IDs — reading the plugins block tells you what the module IS, not how it's configured"
+ - "Zero version literals in module build files: dependencies always go through libs.* aliases; only project coordinate 'version = 1.0.0' (unindented) is exempted by tools/verify-no-version-literals.sh"
+ - "Per-module framework basename: KotlinNativeTarget.binaries.withType().configureEach { baseName = … } pattern is the canonical override point"
+
+requirements-completed: [INFRA-02, INFRA-06]
+
+# Metrics
+duration: ~8min
+completed: 2026-04-24
+---
+
+# Phase 01 Plan 03: Module Build Scripts Wiring Summary
+
+**Rewrote all three module build.gradle.kts files as role declarations applying recipe.* convention plugins; dropped the js target (shared/src/jsMain/ deleted); enabled explicitApi() + 'Shared' framework basename on shared/.**
+
+## Performance
+
+- **Duration:** ~8 min
+- **Started:** 2026-04-24T16:14:27Z
+- **Completed:** 2026-04-24T16:22:17Z
+- **Tasks:** 2
+- **Files modified:** 3 (composeApp, shared, server build.gradle.kts) + 1 deleted (Platform.js.kt)
+
+## Accomplishments
+- **composeApp/build.gradle.kts:** 114 → 28 lines (-75%). Structural blocks (androidTarget, iosArm64/iosSimulatorArm64, jvm, js, wasmJs, android { }, compose.desktop { nativeDistributions }) all removed and inherited from convention plugins. Only 3 source-set dep blocks + 1 debug tooling line remain.
+- **shared/build.gradle.kts:** 55 → 36 lines (-35%). Structural target blocks moved to recipe.kotlin.multiplatform; explicitApi() + KotlinNativeTarget/Framework baseName = "Shared" override added (D-07 / D-12 / PITFALL #10); android {} block kept per Open Question #1.
+- **server/build.gradle.kts:** 23 → 18 lines (-22%). Dependency declarations (logback, ktor-serverCore/Netty/TestHost, kotlin-testJunit) fully relocated into recipe.jvm.server; only module coordinates + mainClass + projects.shared remain.
+- **js target eliminated:** `shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt` deleted (D-01). No `js { browser() }` blocks remain in any module build file.
+- **INFRA-02 payoff visible:** the plugin block in each module now reads as a role declaration (D-06). A future KMP module just needs `plugins { id("recipe.kotlin.multiplatform") }` + sourceSet declarations — no target/SDK copy-pasting.
+- **INFRA-06 structural prerequisite delivered:** recipe.compose.multiplatform is applied ONLY to composeApp/, never to shared/, so Compose deps cannot leak transitively into the shared module's classpath.
+
+## Task Commits
+
+Each task was committed atomically (with `--no-verify` per parallel-executor protocol):
+
+1. **Task 1: Rewrite composeApp + shared build files, delete shared/src/jsMain/** — `d76dcea` (refactor)
+2. **Task 2: Rewrite server build file** — `d316a48` (refactor)
+
+_Note: no test/feat/refactor trio — the plan is marked `type=execute`, not `type=tdd`, and all work is build-script configuration (no production code to test)._
+
+## Files Created/Modified
+- `composeApp/build.gradle.kts` — rewritten: 4 recipe.* plugin IDs, androidMain/commonMain/jvmMain dep blocks, debugImplementation line
+- `shared/build.gradle.kts` — rewritten: 3 plugins (recipe.kotlin.multiplatform + recipe.quality + androidLibrary), explicitApi(), Framework baseName = "Shared" override, android {} retained
+- `server/build.gradle.kts` — rewritten: 2 recipe.* plugin IDs, application { mainClass + JVM args }, implementation(projects.shared)
+- `shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt` — DELETED (D-01 — js target dropped)
+
+## Decisions Made
+- **`libs.versions.*` typed accessor used in module build.gradle.kts rather than `libs.findVersion(...)`** — PITFALL #1 restricts the typed accessor to precompiled plugins; module scripts have full access, so the typed form (`libs.versions.android.compileSdk.get().toInt()`) is correct and preserved from the prior version of `shared/build.gradle.kts`.
+- **Framework baseName override kept local to shared/** — only shared/ needs `"Shared"`; composeApp/ keeps the convention-plugin default `"ComposeApp"`. Hoisting the override into `recipe.kotlin.multiplatform` would require a plugin parameter for a single consumer — not worth the indirection.
+- **`android { }` block retained on shared/** — Open Question #1 in RESEARCH.md defers "do we actually need com.android.library on shared/?" to a future `recipe.android.library` convention plugin. Phase 1 keeps the block applied; a future plan may remove it.
+- **`libs.koin.android` placed in composeApp androidMain, not commonMain** — the `androidContext(...)` helper used by Plan 04's MainApplication lives in koin-android (JVM/Android artifact). commonMain keeps only platform-neutral deps.
+
+## Deviations from Plan
+
+None - plan executed exactly as written.
+
+One minor note (not a deviation, not a failure): `shared/build.gradle.kts` ended at 36 lines vs. the plan's informal `~35-line` target. The single-line delta is the non-negotiable explanatory comment above the `KotlinNativeTarget`/`Framework` block. The plan's `acceptance_criteria` does not set a line cap on `shared/` (only `composeApp/ ≤ 30` which passes at 28 and `server/ ≤ 20` which passes at 18), so all criteria are green.
+
+---
+
+**Total deviations:** 0
+**Impact on plan:** Plan executed as specified. All `` verify blocks pass (grep chain for each module + `tools/verify-no-version-literals.sh` + `tools/verify-shared-pure.sh`).
+
+## Issues Encountered
+None.
+
+## User Setup Required
+None - pure build-script refactor; no external service configuration required.
+
+## Parallel-Wave Coordination Notes
+
+This plan ran as a parallel executor in Wave 2 alongside Plans 02, 04, 05, 06. Per the wave-2 coordination note:
+
+- **No `./gradlew` commands executed in this plan.** The convention plugins referenced by `id("recipe.kotlin.multiplatform")` etc. are created by Plan 02 in a separate worktree; this worktree does NOT see those files. Gradle plugin resolution will succeed after all Wave 2 worktrees merge back to master and Plan 07 runs the full green-build gate.
+- **Verification is entirely grep-based**, matching the plan's `` specification. No runtime build invocation needed at this stage.
+
+## Next Phase Readiness
+
+Ready for downstream plans in Phase 01:
+- **Plan 04 (compose app skeleton)** can now rely on composeApp's `recipe.compose.multiplatform` application — Compose deps (compose.runtime/foundation/material3/ui/components.resources/lifecycle.*compose) flow in via the convention.
+- **Plan 05 (server skeleton)** can rely on server's `recipe.jvm.server` — Ktor server + Flyway + Postgres + serialization flow in via the convention; module only needs to declare `mainClass` and `projects.shared`.
+- **Plan 07 (invariant gate)** will validate the wired build via `./gradlew build` after all Wave 2 worktrees merge back.
+
+Downstream phases (Phase 02+ auth, Phase 05 recipe catalog, etc.) inherit a strict boundary: `shared/commonMain` enforces `explicitApi()` and carries no Compose / Ktor / SQLDelight deps. Any attempt to add forbidden imports will be caught by `tools/verify-shared-pure.sh`.
+
+## Self-Check: PASSED
+
+**Files verified:**
+- FOUND: `composeApp/build.gradle.kts` (28 lines, 4 recipe.* plugin IDs present, no androidTarget/iosArm64/js/nativeDistributions/^android{, libs.koin.android present)
+- FOUND: `shared/build.gradle.kts` (36 lines, 3 plugins present, explicitApi() present, `baseName = "Shared"` present, no js {, android {} retained)
+- FOUND: `server/build.gradle.kts` (18 lines, 2 recipe.* plugin IDs present, mainClass present, projects.shared present, no legacy aliases or deps)
+- MISSING (intentional): `shared/src/jsMain/` directory no longer exists
+
+**Commits verified:**
+- FOUND: `d76dcea` — refactor(01-03): apply recipe.* conventions to composeApp + shared, drop js
+- FOUND: `d316a48` — refactor(01-03): apply recipe.jvm.server + recipe.quality to server module
+
+**Verify scripts:**
+- `tools/verify-no-version-literals.sh` → exit 0 (OK: no version literals outside catalog)
+- `tools/verify-shared-pure.sh` → exit 0 (OK: shared/commonMain is pure)
+
+---
+*Phase: 01-project-infrastructure-module-wiring*
+*Plan: 03*
+*Completed: 2026-04-24*
diff --git a/.planning/phases/01-project-infrastructure-module-wiring/01-04-PLAN.md b/.planning/phases/01-project-infrastructure-module-wiring/01-04-PLAN.md
new file mode 100644
index 0000000..14a8a1e
--- /dev/null
+++ b/.planning/phases/01-project-infrastructure-module-wiring/01-04-PLAN.md
@@ -0,0 +1,495 @@
+---
+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)
+
+
+
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 `