Files
2026-04-29 21:07:49 +02:00

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
01
02
composeApp/build.gradle.kts
shared/build.gradle.kts
server/build.gradle.kts
shared/src/jsMain
true
INFRA-02
INFRA-06
INFRA-02
INFRA-06
truths artifacts key_links
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
path provides min_lines
composeApp/build.gradle.kts Module build applying 4 recipe.* convention plugins + module-only source-set deps (androidMain, commonMain projects.shared, jvmMain desktop) 15
path provides min_lines
shared/build.gradle.kts Module build applying recipe.kotlin.multiplatform + recipe.quality + androidLibrary; enabling explicitApi(); overriding framework baseName to 'Shared'; keeping android { namespace } block 15
path provides min_lines
server/build.gradle.kts Module build applying recipe.jvm.server + recipe.quality; keeping application { mainClass } block and implementation(projects.shared) dep 10
from to via pattern
composeApp/build.gradle.kts build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts plugins { id("recipe.kotlin.multiplatform") } id("recipe.kotlin.multiplatform")
from to via pattern
composeApp/build.gradle.kts build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts plugins { id("recipe.compose.multiplatform") } id("recipe.compose.multiplatform")
from to via pattern
server/build.gradle.kts build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts plugins { id("recipe.jvm.server") } 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.

<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.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:

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:

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 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 <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/.

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:
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 <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>
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 <automated> — 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.

<success_criteria>

  • 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 </success_criteria>
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.