Plan phase 1

This commit is contained in:
2026-04-24 16:21:25 +02:00
parent 7ac1555a4c
commit d6cec3fe07
9 changed files with 4221 additions and 1 deletions

View File

@@ -0,0 +1,576 @@
---
phase: 01-project-infrastructure-module-wiring
plan: 02
type: execute
wave: 1
depends_on: []
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<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)"
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<VersionCatalogsExtension>\\(\\)\\.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\\."
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- These are the canonical excerpts the executor MUST copy verbatim. Line ranges refer to 01-RESEARCH.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<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):
```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 { ... }
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Scaffold build-logic/ included build + 5 precompiled plugins</name>
<files>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</files>
<read_first>
- .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)
</read_first>
<action>
Create the `build-logic/` directory and all 7 files listed in `<files>`. 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.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):
```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 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):
```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<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):
```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<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):
```kotlin
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):
```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<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.
</action>
<verify>
<automated>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<VersionCatalogsExtension>' build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts</automated>
</verify>
<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>
<done>build-logic/ scaffold complete; all 7 files follow canonical patterns; no PITFALL #1/#2/#7/#9/#10 violations detectable via grep.</done>
</task>
<task type="auto">
<name>Task 2: Wire build-logic into root settings.gradle.kts and update root build.gradle.kts</name>
<files>settings.gradle.kts, build.gradle.kts</files>
<read_first>
- 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)
</read_first>
<action>
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).
</action>
<verify>
<automated>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$'</automated>
</verify>
<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>
<done>build-logic/ is discoverable as an included build for plugin resolution; root `build.gradle.kts` declares classloader hints for Spotless + Flyway.</done>
</task>
</tasks>
<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>
<verification>
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.
</verification>
<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>
<output>
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).
</output>