Files
recipe/.planning/phases/01-project-infrastructure-module-wiring/01-02-PLAN.md
2026-04-24 16:21:25 +02:00

30 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 02 execute 1
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
true
INFRA-02
INFRA-02
truths artifacts key_links
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<VersionCatalogsExtension>().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)
path provides
build-logic/settings.gradle.kts Included-build settings with shared catalog access (from files("../gradle/libs.versions.toml"))
path provides
build-logic/build.gradle.kts kotlin-dsl plugin + compileOnly(asDependency()) entries for every alias-based plugin referenced by precompiled plugins
path provides
build-logic/src/main/kotlin/recipe.quality.gradle.kts Spotless + ktlint + allWarningsAsErrors safety net (D-10 / D-11)
path provides
build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts D-05 target matrix + JVM toolchain + common deps + allWarningsAsErrors (D-07, D-08, D-11)
path provides
build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts Compose MP plugin + hot-reload + Compose deps for commonMain (layered on KMP)
path provides
build-logic/src/main/kotlin/recipe.android.application.gradle.kts com.android.application + namespace + SDK versions (composeApp only)
path provides
build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts kotlin(jvm) + Ktor + Flyway + server deps (server only)
path provides
settings.gradle.kts Root settings with pluginManagement { includeBuild("build-logic") }
path provides
build.gradle.kts Root build with apply-false entries for spotless + flywayPlugin (classloader hint)
from to via pattern
build-logic/src/main/kotlin/recipe.*.gradle.kts gradle/libs.versions.toml VersionCatalogsExtension.named("libs") extensions.getByType<VersionCatalogsExtension>().named("libs")
from to via pattern
Plan 03 module build files build-logic/src/main/kotlin/recipe.*.gradle.kts plugins { id("recipe.kotlin.multiplatform") } 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.

<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 @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):

import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

val libs = extensions.getByType<VersionCatalogsExtension>().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):

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

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

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

plugins {
    `kotlin-dsl`
}

dependencies {
    compileOnly(libs.plugins.kotlinMultiplatform.asDependency())
    compileOnly(libs.plugins.androidApplication.asDependency())
    compileOnly(libs.plugins.androidLibrary.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<PluginDependency>.asDependency(): Provider<String> =
    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):

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 WITHOUT recipe.kotlin.multiplatform
// (e.g. a future pure-JVM utility), ensure allWarningsAsErrors still applies.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().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):

// 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<VersionCatalogsExtension>().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):

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<VersionCatalogsExtension>().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):

import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

plugins {
    id("com.android.application")
}

val libs = extensions.getByType<VersionCatalogsExtension>().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):

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<VersionCatalogsExtension>().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 <acceptance_criteria> - 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<PluginDependency>.asDependency() extension function - build-logic/build.gradle.kts has exactly 10 compileOnly(libs.plugins.*.asDependency()) calls (kotlinMultiplatform, androidApplication, androidLibrary, composeMultiplatform, composeCompiler, composeHotReload, kotlinJvm, ktor, spotless, flywayPlugin) - 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<VersionCatalogsExtension>().named("libs") </acceptance_criteria> 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:

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:

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$' <acceptance_criteria> - 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 </acceptance_criteria> build-logic/ is discoverable as an included build for plugin resolution; root build.gradle.kts declares classloader hints for Spotless + Flyway.

<threat_model>

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 <acceptance_criteria> 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.
</threat_model>
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 <automated> verify blocks to keep them fast; run them once manually if a later plan fails unexpectedly.

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-02-SUMMARY.md` recording: file tree under `build-logic/`, any deviations from canonical excerpts (expected: none), and the final plugin ID list (10 applies from recipe-family + spotless/flyway).