docs(01): capture phase context
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Phase 1: Project Infrastructure & Module Wiring - Context
|
||||
|
||||
**Gathered:** 2026-04-24
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Stand up a KMP client + Ktor server whose build is "boring correct" from day 1 — Gradle version catalog, `build-logic/` convention plugins, iOS binary flags, a pure-Kotlin `shared/` module, foundational DI + logging bootstrap, and a minimally-running Ktor server — so every later phase slots into an already-configured system. Scope is infrastructure only; no feature logic, no auth, no DB tables, no UI beyond the template screens.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Target matrix
|
||||
- **D-01:** Drop the `js` target from `composeApp` and `shared`. Keep `wasmJs` as the strategic future-web bet (per PROJECT.md "possible future target").
|
||||
- **D-02:** Skip `iosX64` (Intel simulator / iPhone 5S-SE1). User is on Apple Silicon; no Intel-Mac contributors anticipated. Saves a full iOS compile per build.
|
||||
- **D-03:** Keep `jvm` target in `composeApp` for Desktop — **as a dev tool only** (hot-reload iteration loop). No Compose Desktop packaging config; not a release surface; not a v1 deliverable per PROJECT.md.
|
||||
- **D-04:** `shared/` ships the exact same target set as `composeApp`: `androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs`. Plus `jvm` covers the server dependency.
|
||||
- **D-05:** Final target matrix repo-wide: `androidTarget, iosArm64, iosSimulatorArm64, jvm (Desktop + Server), wasmJs`.
|
||||
|
||||
### Convention plugins (build-logic/)
|
||||
- **D-06:** Fine-grained plugin split (5 plugins). Each module applies only what it needs:
|
||||
- `recipe.kotlin.multiplatform` — KMP target matrix + JVM toolchain + common-test deps
|
||||
- `recipe.compose.multiplatform` — Compose Multiplatform setup (layers on top of KMP)
|
||||
- `recipe.android.application` — Android-app-only config (namespace, compileSdk, minSdk, targetSdk from catalog)
|
||||
- `recipe.jvm.server` — Ktor server JVM config
|
||||
- `recipe.quality` — Spotless + ktlint + compiler strictness (reusable across all modules)
|
||||
- **D-07:** `recipe.kotlin.multiplatform` locks in: the D-05 target set, JVM toolchain, framework basename convention (`ComposeApp` / `Shared`), and `kotlin-test` as a common-test dep. New KMP modules apply this plugin and get everything.
|
||||
- **D-08:** JVM toolchain: **JVM 21** for server, desktop, and `shared/jvm`. Android bytecode target stays **JVM 11** (Android 7 minSdk constraint per template). Document this split in the convention plugin comments.
|
||||
- **D-09:** **All library versions live in `gradle/libs.versions.toml`.** Hard rule: grep for a non-test version literal inside any `build.gradle.kts` returns zero matches. This is INFRA-01 Success Criterion #2. Plugin versions also routed through the catalog (aliases).
|
||||
|
||||
### Code-quality toolchain (recipe.quality plugin)
|
||||
- **D-10:** Minimal baseline — ship ktlint via **Spotless** only. Spotless handles Kotlin + Gradle files + markdown. Commands: `./gradlew spotlessCheck`, `./gradlew spotlessApply`. No Detekt, no Konsist in Phase 1.
|
||||
- **D-11:** `allWarningsAsErrors = true` everywhere (configured in `recipe.kotlin.multiplatform`). Any Kotlin/compiler warning fails the build; forces conscious suppression rather than silent drift.
|
||||
- **D-12:** `explicitApi()` **strict on `shared/` only**. `shared/` is structurally a library (consumed by both composeApp and server as a wire-format contract); `composeApp` and `server` are app code and stay on Kotlin defaults. Configured in `shared/build.gradle.kts` directly, not in the KMP plugin (app modules shouldn't inherit it).
|
||||
- **D-13:** **No git hooks.** `./gradlew check` is the local gate; CI gate deferred to Phase 11 (deployment). Local hooks add commit friction and are trivially bypassed.
|
||||
|
||||
### Phase 1 "running-but-empty" scope — what's wired beyond the template
|
||||
- **D-14:** **Koin bootstrap.** Add Koin deps (`koin-core`, `koin-compose`, `koin-compose-viewmodel`) via `recipe.kotlin.multiplatform`. Call `startKoin { modules(appModule) }` inside `App()` for composeApp and `MainViewController` for iOS. Ship an empty `appModule` placeholder in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`. Phase 2 adds `authModule`; Phase 4 adds `syncModule`; etc.
|
||||
- **D-15:** **Kermit logger bootstrap.** Add Kermit dep via `recipe.kotlin.multiplatform`. Set a single top-level tag (`"recipe"`) during app init. Available from day 1 for subsequent phases.
|
||||
- **D-16:** **Server: `/health` endpoint + Flyway scaffold + Postgres conn config.**
|
||||
- `GET /health` returns 200 with a trivial JSON body.
|
||||
- Flyway Gradle plugin + runtime dep wired into `server/build.gradle.kts` via `recipe.jvm.server`; `src/main/resources/db/migration/` directory created (empty). Phase 3 drops `V1__init.sql` into an already-working migrator.
|
||||
- `application.conf` reads `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD` from env with localhost defaults matching docker-compose.
|
||||
- Server starts and connects to Postgres on boot; fails loudly (not silently) if Postgres is unreachable.
|
||||
- **D-17:** **`docker-compose.yml` at repo root** defines a `postgres:16` service with a named volume. `README.md` gets a "Local development" section. Phase 3 does not have to litigate local-Postgres setup. Authentik stays on user's homelab (not in docker-compose) but the compose file is the handle for future local services if they're ever needed.
|
||||
|
||||
### Locked infrastructure hygiene (from PROJECT.md, enforced in Phase 1)
|
||||
- **D-18:** iOS binary flags added to `gradle.properties`: `kotlin.native.binary.objcDisposeOnMain=false` and `kotlin.native.binary.gc=cms` (INFRA-03, PITFALLS.md #1).
|
||||
- **D-19:** `shared/commonMain` stays pure: domain models + `@Serializable` DTOs only; no Ktor, no Compose, no SQLDelight imports. Phase 1 ships an empty package scaffold under `dev.ulfrx.recipe.shared` ready for Phase 2+ DTOs (INFRA-06).
|
||||
- **D-20:** Namespace `dev.ulfrx.recipe` (package root). Framework basename `ComposeApp` for iOS. No feature modules in v1.
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact ordering of plugin application inside each `build.gradle.kts`
|
||||
- Specific `spotless { kotlin { ktlint(...) } }` ruleset version (pick latest stable from catalog)
|
||||
- Whether `application.conf` or `ApplicationConfig.kt` code owns env-var parsing
|
||||
- Flyway `cleanDisabled` and `baselineOnMigrate` flag choices (use sane defaults for dev)
|
||||
- Whether Koin bootstrap in `MainViewController` uses `KoinApplication` vs `startKoin` (iOS-specific idiom)
|
||||
- Whether `docker-compose.yml` uses a `.env` file or inlines localhost defaults
|
||||
- The exact sentinel JSON body for `/health` (empty object is fine)
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Product + scope anchors
|
||||
- `.planning/PROJECT.md` — Locked tech stack (§ Key Decisions), constraints, module structure rules
|
||||
- `.planning/REQUIREMENTS.md` — INFRA-01, INFRA-02, INFRA-03, INFRA-06 are the in-scope requirements for this phase
|
||||
- `.planning/ROADMAP.md` § "Phase 1: Project Infrastructure & Module Wiring" — phase goal + 5 success criteria; ordering rationale for subsequent phases
|
||||
|
||||
### Architecture + pitfalls
|
||||
- `.planning/research/ARCHITECTURE.md` — Recommended project structure (§ Recommended Project Structure) defines the `composeApp/commonMain` package layout that Phase 1 scaffolds; § Build Order Implication explains why the foundation-first order matters
|
||||
- `.planning/research/PITFALLS.md` — Phase 1 must prevent pitfalls #1 (K/N GC + objcDisposeOnMain), #2 (legacy freeze/SharedImmutable — Kotlin 2.x only), #5 (newSuspendedTransaction, not relevant in Phase 1 but plugin must not preclude it), #6 (DSL-only Exposed, infra impact only)
|
||||
- `.planning/research/SUMMARY.md` § "Phase 1: Project infrastructure + module wiring" — executive summary of the research-driven rationale
|
||||
|
||||
### Project convention
|
||||
- `CLAUDE.md` — Non-negotiable conventions (§ Non-negotiable conventions). Items #5 (Exposed DSL only), #7 (iOS binary flags day 1), #8 (shared/commonMain stays light), #9 (strings externalized from day 1 — Phase 1 scaffold only, real copy in Phase 11) all touch Phase 1.
|
||||
|
||||
No external ADRs or specs yet — project is greenfield; decisions flow from PROJECT.md + research/ files.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable assets (what the template already gives us)
|
||||
- `gradle/libs.versions.toml` exists and is the catalog. Needs to grow; does not need to be created.
|
||||
- `gradle.properties` exists with basic Gradle memory + Android settings. **Missing iOS binary flags** (D-18 adds them).
|
||||
- `settings.gradle.kts` already enables `TYPESAFE_PROJECT_ACCESSORS` — keep it.
|
||||
- Compose Multiplatform hot reload already works for Desktop (commit c50d747). The `recipe.compose.multiplatform` convention plugin should preserve that wiring.
|
||||
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` is the template App(). Koin `startKoin { }` call goes here.
|
||||
- `server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` is the template Ktor module. `/health` route + Flyway bootstrap go here.
|
||||
- `iosApp/iosApp/iOSApp.swift` + `ContentView.swift` — the MainViewController hookup for iOS lives here; that's where iOS-side `startKoin` + `ComposeUIViewController` wiring lands.
|
||||
|
||||
### Established patterns
|
||||
- JetBrains KMP template conventions (plugin application style, source-set DSL) — Phase 1 refactors into convention plugins but must not break template compatibility (future template updates are an informal escape hatch).
|
||||
- `gradle/libs.versions.toml` uses `version.ref = "..."` aliases — continue that pattern; do not introduce inline versions.
|
||||
|
||||
### Integration points
|
||||
- Each module's `build.gradle.kts` replaces its `plugins { alias(...) }` block with `plugins { id("recipe.kotlin.multiplatform"); id("recipe.quality"); ... }`. The actual alias-based plugins (`kotlinMultiplatform`, `composeMultiplatform`, etc.) are applied *inside* the convention plugins, so modules no longer touch `libs.plugins.*`.
|
||||
- Root `build.gradle.kts` keeps its `apply false` declarations for now (Gradle's plugin classloader hint); convention plugins rely on those declarations being present in the root build.
|
||||
- `build-logic/` is its own included build (`includeBuild("build-logic")` in `settings.gradle.kts`) — standard Gradle pattern, not a regular module.
|
||||
|
||||
### What must NOT change in Phase 1
|
||||
- Package namespace (`dev.ulfrx.recipe`) — locked in CLAUDE.md and every existing file.
|
||||
- Android minSdk 24 / compileSdk 36 / targetSdk 36 — locked in `libs.versions.toml`.
|
||||
- Kotlin version (2.3.20), AGP (8.11.2), Compose Multiplatform (1.10.3), Ktor (3.4.1) — current template versions, upgraded only if catalog-wide bump becomes necessary.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- **"Fine-grained conventions" means a module's plugins block reads like a role declaration.** `composeApp/build.gradle.kts` should literally say: "I am a Kotlin Multiplatform module, I use Compose, I am an Android application, I follow the quality rules." No hidden Compose config leaking into `shared/`.
|
||||
- **`./gradlew build` succeeds green** is the verification ritual. Any deviation from Phase 1 AC#1 is a regression. Every plan in this phase should end with that check.
|
||||
- **Android minSdk 24 stays.** Partner's phones are modern enough; Android is secondary anyway. Revisit only if a library requires higher.
|
||||
- **docker-compose.yml is dev-ergonomics, not deploy infra.** Phase 11 handles the real homelab deploy (separate compose file on the homelab, alongside Authentik).
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **Detekt static analysis** — skip day 1; add only if code review starts missing the same classes of bug. Revisit criterion: "we've had 3+ PR comments that Detekt would have caught."
|
||||
- **Konsist architecture fitness tests** — revisit ~Phase 4 (SyncEngine) when cross-layer rules like "repositories never import Ktor Client" or "no HTTP from composeApp/ui/" become meaningful to police. Pattern 2 in ARCHITECTURE.md is the first rule that deserves a fitness test.
|
||||
- **CI pipeline (GitHub Actions or homelab runner)** — Phase 11 per ROADMAP.md. Phase 1 is single-dev, local-build-only.
|
||||
- **Git hooks** — considered and explicitly rejected; revisit only if local formatting drift becomes a recurring problem.
|
||||
- **explicitApi for composeApp and server** — considered; rejected because both are app code, not libraries. Only `shared/` gets the discipline.
|
||||
- **iosX64 target** — rejected; revisit only if an Intel-Mac contributor joins.
|
||||
- **`js` target** — rejected; `wasmJs` covers the future-web ambition alone.
|
||||
- **Compose Desktop packaging (dmg/msi/exe)** — Desktop is dev-tool only in v1; full packaging is out of scope entirely.
|
||||
- **Konsist, Detekt, CI** listed above are the candidates most likely to be revisited first.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-project-infrastructure-module-wiring*
|
||||
*Context gathered: 2026-04-24*
|
||||
Reference in New Issue
Block a user