23 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, requirements_addressed, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | requirements_addressed | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-project-infrastructure-module-wiring | 03 | execute | 2 |
|
|
true |
|
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md @.planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md @.planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md @.planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md @composeApp/build.gradle.kts @shared/build.gradle.kts @server/build.gradle.kts @CLAUDE.mdFrom gradle/libs.versions.toml (Plan 01 extended):
libs.plugins.androidLibrary— still referenced as alias inside shared/build.gradle.ktslibs.compose.uiToolingPreview— referenced from composeApp/build.gradle.kts module-specific depslibs.androidx.activity.compose— referenced from composeApp androidMain depslibs.kotlinx.coroutinesSwing— referenced from composeApp jvmMain depslibs.compose.uiTooling— referenced from composeApp debugImplementationlibs.koin.android— NEW alias (Plan 01) for MainApplication'sandroidContext(...)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, allWarningsAsErrorsrecipe.compose.multiplatform— applies Compose MP + hot-reload on top of KMP, adds compose-* deps to commonMainrecipe.android.application— applies com.android.application, sets namespace + SDK versionsrecipe.jvm.server— applies kotlin(jvm) + io.ktor.plugin + flyway + all server deps + quoted-config dependency blockrecipe.quality— applies Spotless + allWarningsAsErrors safety net
Rewrite 1: composeApp/build.gradle.kts — replace entire file content with:
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 intorecipe.kotlin.multiplatform - DROP
commonMain.dependenciesCompose entries (lines 52-62 — compose.runtime, compose.foundation, compose.material3, compose.ui, compose.components.resources, androidx.lifecycle.viewmodelCompose, androidx.lifecycle.runtimeCompose) — moved intorecipe.compose.multiplatform. KEEPimplementation(projects.shared)and the module-onlyimplementation(libs.compose.uiToolingPreview)(the preview tooling is needed by@Previewannotations in composeApp's common code). - DROP
commonTest.dependencies { implementation(libs.kotlin.test) }(lines 63-65) — moved intorecipe.kotlin.multiplatform - DROP the entire
android { ... }block (lines 73-98) — moved intorecipe.android.application - DROP
compose.desktop { application { ... nativeDistributions { ... } } }(lines 104-114) — D-03 says no desktop packaging
ADDITIONS:
- ADD
implementation(libs.koin.android)toandroidMain.dependencies(Plan 04's MainApplication.kt callsandroidContext(...)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 depsjvmMain.dependencies { implementation(compose.desktop.currentOs); implementation(libs.kotlinx.coroutinesSwing) }— Desktop-only depsdependencies { debugImplementation(libs.compose.uiTooling) }— Android debug-only tooling
Rewrite 2: shared/build.gradle.kts — replace entire file content with:
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<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework>().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 withid("recipe.kotlin.multiplatform")+ keptalias(libs.plugins.androidLibrary) - DROP the entire
kotlin { androidTarget { ... } iosArm64() iosSimulatorArm64() jvm { ... } js { ... } wasmJs { ... } ... }structural block (lines 9-41) — moved intorecipe.kotlin.multiplatform - DROP
js { browser() }(lines 25-27) — D-01
ADDITIONS:
- ADD
explicitApi()inside thekotlin { }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, keepcom.android.libraryapplied in Phase 1 (deferring the "do we need it" question to a futurerecipe.android.libraryplugin)
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
<acceptance_criteria>
- 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)
</acceptance_criteria>
Both module builds apply recipe.* conventions; js target source dir deleted; explicitApi + Shared basename set on shared/.
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. Theapplicationplugin is applied byrecipe.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 intorecipe.jvm.server.
KEEP:
group = "dev.ulfrx.recipe"andversion = "1.0.0"(module coordinates — per-module concern)application { mainClass.set(...) }+applicationDefaultJvmArgs(per-module config — theapplicationplugin is applied byrecipe.jvm.serverbut 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
<acceptance_criteria>
- 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
</acceptance_criteria>
server build applies recipe.jvm.server + recipe.quality; module-only config (mainClass, shared dep) preserved.
<threat_model>
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 <automated> 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 <acceptance_criteria> 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. |
| </threat_model> |
- All three
tools/verify-*.shscripts exit 0 after rewrites. shared/src/jsMain/directory no longer exists.composeApp/build.gradle.ktsshrinks from 114 to ~30 lines — INFRA-02 payoff visible.shared/build.gradle.ktsshrinks from 55 to ~35 lines and now setsexplicitApi().
Optional sanity check (NOT in <automated> — Plan 07 runs the full gate):
./gradlew :composeApp:help -qemits 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./gradlewtargets.
<success_criteria>
composeApp/build.gradle.ktsapplies 4 recipe.* IDs and contains NOkotlin { androidTarget { ... } ... }structural block and NOandroid { ... }block and NOnativeDistributionsshared/build.gradle.ktsapplies 3 plugins (2 recipe.* + androidLibrary), enablesexplicitApi(), overrides baseName to"Shared"server/build.gradle.ktsapplies 2 recipe.* IDs and keeps onlyapplication { mainClass }+implementation(projects.shared)shared/src/jsMain/deletedtools/verify-no-version-literals.shexits 0tools/verify-shared-pure.shexits 0 </success_criteria>