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 |
|
true |
|
|
|
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.mdPlugin applications reference (01-PATTERNS.md and 01-RESEARCH.md):
id("recipe.quality")→ from .gradle.kts file namedrecipe.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 { ... }
}
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.
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 { }blockplugins { 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> |
tools/verify-no-version-literals.shstill exits 0 (build-logic/build.gradle.kts is explicitly excluded by the script — theasDependency()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 buildwill still work against the EXISTING module build files (which have not yet been refactored).
Optional fast sanity check (if needed):
./gradlew --helpexits 0 (provessettings.gradle.ktsstill parses)../gradlew help(without args) exits 0 (provesincludeBuildis 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 infiles_modified) settings.gradle.ktshasincludeBuild("build-logic")insidepluginManagement { }build.gradle.ktshas 10apply falseentries (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.shcontinues to exit 0 </success_criteria>