--- phase: 01-project-infrastructure-module-wiring plan: 03 subsystem: infra tags: [gradle, kmp, convention-plugins, compose-multiplatform, ktor-server, android-library, explicitApi, ios-framework] # Dependency graph requires: - phase: 01-project-infrastructure-module-wiring provides: "Plan 01 extended libs.versions.toml (koin.android alias); Plan 02 created 5 recipe.* convention plugins in build-logic/" provides: - "composeApp/build.gradle.kts reduced from 114 to 28 lines; role-declaration plugin block applying recipe.kotlin.multiplatform + recipe.compose.multiplatform + recipe.android.application + recipe.quality" - "shared/build.gradle.kts reduced from 55 to 36 lines; applies recipe.kotlin.multiplatform + recipe.quality + androidLibrary; enables explicitApi(); overrides iOS framework baseName to 'Shared'" - "server/build.gradle.kts reduced from 23 to 18 lines; applies recipe.jvm.server + recipe.quality; retains only module-specific mainClass + projects.shared dep" - "js target fully removed: shared/src/jsMain/ directory deleted (D-01)" - "iosX64 remains absent across all modules (D-02)" - "INFRA-02 structural payoff visible: adding a new KMP module henceforth requires only plugins { id('recipe.kotlin.multiplatform') } + sourceSet declarations" - "INFRA-06 structural prerequisite: shared/ no longer applies recipe.compose.multiplatform, so Compose cannot leak transitively" affects: [02-auth, 03-households, 04-sync-skeleton, 05-recipe-catalog, 10-ui-chrome] # Tech tracking tech-stack: added: [] # Plan 03 is pure refactor — all libraries/tools already added in Plans 01/02 patterns: - "Role-declaration plugin blocks (D-06): module build.gradle.kts plugins {} lists only recipe.* IDs + module-specific aliases (e.g. androidLibrary on shared/)" - "Per-module override pattern: shared/ overrides framework baseName by targeting KotlinNativeTarget + Framework directly in the module, not from the convention plugin (D-07 / PITFALL #10)" - "Module-specific dep retention: jvmMain compose.desktop.currentOs + kotlinx.coroutinesSwing stay in composeApp; android debug-only libs.compose.uiTooling stays as debugImplementation" key-files: created: [] modified: - "composeApp/build.gradle.kts — rewritten: 4 recipe.* plugin IDs + 3-source-set dep block + 1 debug tooling line" - "shared/build.gradle.kts — rewritten: 3 plugins + explicitApi() + Framework baseName override + android {} block retained" - "server/build.gradle.kts — rewritten: 2 recipe.* plugin IDs + application {} + projects.shared dep" - "shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt — DELETED (D-01 drops js target)" key-decisions: - "Keep android { namespace = 'dev.ulfrx.recipe.shared' } block applied in Phase 1 per Open Question #1 (com.android.library retained; future recipe.android.library convention plugin deferred)" - "libs.versions.* typed accessor used directly in module build.gradle.kts (not libs.findVersion) — PITFALL #1 only applies to precompiled plugin scripts, not module scripts" - "libs.koin.android added to composeApp androidMain (not commonMain) — Koin's androidContext(...) lives in the android-specific artifact; commonMain stays platform-neutral" - "Framework baseName override placed in the module, not hoisted into recipe.kotlin.multiplatform — shared/ is the only module needing 'Shared' (composeApp keeps convention default 'ComposeApp'), so keeping it local avoids a plugin parameter" patterns-established: - "Plugin role declaration: each module build.gradle.kts opens with id('recipe.') IDs — reading the plugins block tells you what the module IS, not how it's configured" - "Zero version literals in module build files: dependencies always go through libs.* aliases; only project coordinate 'version = 1.0.0' (unindented) is exempted by tools/verify-no-version-literals.sh" - "Per-module framework basename: KotlinNativeTarget.binaries.withType().configureEach { baseName = … } pattern is the canonical override point" requirements-completed: [INFRA-02, INFRA-06] # Metrics duration: ~8min completed: 2026-04-24 --- # Phase 01 Plan 03: Module Build Scripts Wiring Summary **Rewrote all three module build.gradle.kts files as role declarations applying recipe.* convention plugins; dropped the js target (shared/src/jsMain/ deleted); enabled explicitApi() + 'Shared' framework basename on shared/.** ## Performance - **Duration:** ~8 min - **Started:** 2026-04-24T16:14:27Z - **Completed:** 2026-04-24T16:22:17Z - **Tasks:** 2 - **Files modified:** 3 (composeApp, shared, server build.gradle.kts) + 1 deleted (Platform.js.kt) ## Accomplishments - **composeApp/build.gradle.kts:** 114 → 28 lines (-75%). Structural blocks (androidTarget, iosArm64/iosSimulatorArm64, jvm, js, wasmJs, android { }, compose.desktop { nativeDistributions }) all removed and inherited from convention plugins. Only 3 source-set dep blocks + 1 debug tooling line remain. - **shared/build.gradle.kts:** 55 → 36 lines (-35%). Structural target blocks moved to recipe.kotlin.multiplatform; explicitApi() + KotlinNativeTarget/Framework baseName = "Shared" override added (D-07 / D-12 / PITFALL #10); android {} block kept per Open Question #1. - **server/build.gradle.kts:** 23 → 18 lines (-22%). Dependency declarations (logback, ktor-serverCore/Netty/TestHost, kotlin-testJunit) fully relocated into recipe.jvm.server; only module coordinates + mainClass + projects.shared remain. - **js target eliminated:** `shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt` deleted (D-01). No `js { browser() }` blocks remain in any module build file. - **INFRA-02 payoff visible:** the plugin block in each module now reads as a role declaration (D-06). A future KMP module just needs `plugins { id("recipe.kotlin.multiplatform") }` + sourceSet declarations — no target/SDK copy-pasting. - **INFRA-06 structural prerequisite delivered:** recipe.compose.multiplatform is applied ONLY to composeApp/, never to shared/, so Compose deps cannot leak transitively into the shared module's classpath. ## Task Commits Each task was committed atomically (with `--no-verify` per parallel-executor protocol): 1. **Task 1: Rewrite composeApp + shared build files, delete shared/src/jsMain/** — `d76dcea` (refactor) 2. **Task 2: Rewrite server build file** — `d316a48` (refactor) _Note: no test/feat/refactor trio — the plan is marked `type=execute`, not `type=tdd`, and all work is build-script configuration (no production code to test)._ ## Files Created/Modified - `composeApp/build.gradle.kts` — rewritten: 4 recipe.* plugin IDs, androidMain/commonMain/jvmMain dep blocks, debugImplementation line - `shared/build.gradle.kts` — rewritten: 3 plugins (recipe.kotlin.multiplatform + recipe.quality + androidLibrary), explicitApi(), Framework baseName = "Shared" override, android {} retained - `server/build.gradle.kts` — rewritten: 2 recipe.* plugin IDs, application { mainClass + JVM args }, implementation(projects.shared) - `shared/src/jsMain/kotlin/dev/ulfrx/recipe/Platform.js.kt` — DELETED (D-01 — js target dropped) ## Decisions Made - **`libs.versions.*` typed accessor used in module build.gradle.kts rather than `libs.findVersion(...)`** — PITFALL #1 restricts the typed accessor to precompiled plugins; module scripts have full access, so the typed form (`libs.versions.android.compileSdk.get().toInt()`) is correct and preserved from the prior version of `shared/build.gradle.kts`. - **Framework baseName override kept local to shared/** — only shared/ needs `"Shared"`; composeApp/ keeps the convention-plugin default `"ComposeApp"`. Hoisting the override into `recipe.kotlin.multiplatform` would require a plugin parameter for a single consumer — not worth the indirection. - **`android { }` block retained on shared/** — Open Question #1 in RESEARCH.md defers "do we actually need com.android.library on shared/?" to a future `recipe.android.library` convention plugin. Phase 1 keeps the block applied; a future plan may remove it. - **`libs.koin.android` placed in composeApp androidMain, not commonMain** — the `androidContext(...)` helper used by Plan 04's MainApplication lives in koin-android (JVM/Android artifact). commonMain keeps only platform-neutral deps. ## Deviations from Plan None - plan executed exactly as written. One minor note (not a deviation, not a failure): `shared/build.gradle.kts` ended at 36 lines vs. the plan's informal `~35-line` target. The single-line delta is the non-negotiable explanatory comment above the `KotlinNativeTarget`/`Framework` block. The plan's `acceptance_criteria` does not set a line cap on `shared/` (only `composeApp/ ≤ 30` which passes at 28 and `server/ ≤ 20` which passes at 18), so all criteria are green. --- **Total deviations:** 0 **Impact on plan:** Plan executed as specified. All `` verify blocks pass (grep chain for each module + `tools/verify-no-version-literals.sh` + `tools/verify-shared-pure.sh`). ## Issues Encountered None. ## User Setup Required None - pure build-script refactor; no external service configuration required. ## Parallel-Wave Coordination Notes This plan ran as a parallel executor in Wave 2 alongside Plans 02, 04, 05, 06. Per the wave-2 coordination note: - **No `./gradlew` commands executed in this plan.** The convention plugins referenced by `id("recipe.kotlin.multiplatform")` etc. are created by Plan 02 in a separate worktree; this worktree does NOT see those files. Gradle plugin resolution will succeed after all Wave 2 worktrees merge back to master and Plan 07 runs the full green-build gate. - **Verification is entirely grep-based**, matching the plan's `` specification. No runtime build invocation needed at this stage. ## Next Phase Readiness Ready for downstream plans in Phase 01: - **Plan 04 (compose app skeleton)** can now rely on composeApp's `recipe.compose.multiplatform` application — Compose deps (compose.runtime/foundation/material3/ui/components.resources/lifecycle.*compose) flow in via the convention. - **Plan 05 (server skeleton)** can rely on server's `recipe.jvm.server` — Ktor server + Flyway + Postgres + serialization flow in via the convention; module only needs to declare `mainClass` and `projects.shared`. - **Plan 07 (invariant gate)** will validate the wired build via `./gradlew build` after all Wave 2 worktrees merge back. Downstream phases (Phase 02+ auth, Phase 05 recipe catalog, etc.) inherit a strict boundary: `shared/commonMain` enforces `explicitApi()` and carries no Compose / Ktor / SQLDelight deps. Any attempt to add forbidden imports will be caught by `tools/verify-shared-pure.sh`. ## Self-Check: PASSED **Files verified:** - FOUND: `composeApp/build.gradle.kts` (28 lines, 4 recipe.* plugin IDs present, no androidTarget/iosArm64/js/nativeDistributions/^android{, libs.koin.android present) - FOUND: `shared/build.gradle.kts` (36 lines, 3 plugins present, explicitApi() present, `baseName = "Shared"` present, no js {, android {} retained) - FOUND: `server/build.gradle.kts` (18 lines, 2 recipe.* plugin IDs present, mainClass present, projects.shared present, no legacy aliases or deps) - MISSING (intentional): `shared/src/jsMain/` directory no longer exists **Commits verified:** - FOUND: `d76dcea` — refactor(01-03): apply recipe.* conventions to composeApp + shared, drop js - FOUND: `d316a48` — refactor(01-03): apply recipe.jvm.server + recipe.quality to server module **Verify scripts:** - `tools/verify-no-version-literals.sh` → exit 0 (OK: no version literals outside catalog) - `tools/verify-shared-pure.sh` → exit 0 (OK: shared/commonMain is pure) --- *Phase: 01-project-infrastructure-module-wiring* *Plan: 03* *Completed: 2026-04-24*