--- 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 After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-03-SUMMARY.md` recording: final LOC of each module build file (target: composeApp ≤30, shared ≤35, server ≤20), any deviations from the canonical patterns (expected: none), and confirmation that `shared/src/jsMain/` is gone.