---
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.<role>') 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<Framework>().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 `<automated>` 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 `<automated>` 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*
