docs(01): capture phase context

This commit is contained in:
2026-04-24 15:27:13 +02:00
parent c50d747cf6
commit 68e4a5637a
2 changed files with 360 additions and 0 deletions

View File

@@ -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*

View File

@@ -0,0 +1,216 @@
# Phase 1: Project Infrastructure & Module Wiring - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-04-24
**Phase:** 01-project-infrastructure-module-wiring
**Areas discussed:** Target matrix, Convention plugin split, Code-quality toolchain, "Running-but-empty" scope
---
## Target matrix
### Q1: JS target in composeApp + shared — drop it?
| Option | Description | Selected |
|--------|-------------|----------|
| Drop js, keep wasmJs | PROJECT.md mentions Wasm but not js; js is legacy Kotlin/JS path | ✓ |
| Keep both js and wasmJs | Preserve template exactly; zero risk of Kotlin/JS regression on future merges | |
| Drop both | Strictest minimum; re-add wasmJs only if web becomes real | |
**User's choice:** Drop js, keep wasmJs.
### Q2: iosX64 target — include?
| Option | Description | Selected |
|--------|-------------|----------|
| Skip iosX64 | User is on Apple Silicon; iosArm64 + iosSimulatorArm64 sufficient | ✓ |
| Add iosX64 for safety | Intel-Mac safety net; costs a second iOS compile per build | |
**User's choice:** Skip iosX64.
### Q3: Desktop JVM target — role?
| Option | Description | Selected |
|--------|-------------|----------|
| Dev-tool only, no shipped artifact | Hot-reload loop retained; no packaging; PROJECT.md alignment | ✓ |
| Full desktop app, packaged + shipped | dmg/msi/exe as release surface; out of PROJECT.md scope | |
| Drop desktop target entirely | Simplest; loses hot reload | |
**User's choice:** Dev-tool only, no shipped artifact.
### Q4: shared/ target set — mirror composeApp?
| Option | Description | Selected |
|--------|-------------|----------|
| Mirror composeApp exactly | Same target set; consistent dep graph | ✓ |
| Superset: ship everything KMP supports | Every target just in case; build cost | |
| Minimum: only what's used today | Strictest diet; same as Option 1 if we drop js | |
**User's choice:** Mirror composeApp exactly.
---
## Convention plugin split
### Q1: How granular should the convention plugins be?
| Option | Description | Selected |
|--------|-------------|----------|
| Fine-grained (45 plugins) | recipe.kotlin.multiplatform + .compose.multiplatform + .android.application + .jvm.server + .quality | ✓ |
| Coarse (2 plugins) | recipe.kmp + recipe.server; leaks Compose config into shared | |
| Monolith (1 plugin) | Single recipe.conventions with conditional logic | |
**User's choice:** Fine-grained (45 plugins). Preview showed the role-declaration pattern in each module's plugins block.
### Q2: What does the KMP convention plugin lock in?
| Option | Description | Selected |
|--------|-------------|----------|
| Targets + toolchain + common test deps | New KMP module = apply plugin, done | ✓ |
| Targets + toolchain only | Thinner plugin, more repetition downstream | |
| Everything incl. Koin + Kermit wiring | Upfront convenience, invasive over time | |
**User's choice:** Targets + toolchain + common test deps.
### Q3: JVM toolchain version?
| Option | Description | Selected |
|--------|-------------|----------|
| JVM 21 everywhere, androidTarget stays JVM 11 | Split kept; modern JDK on server, Android constrained | ✓ |
| JVM 17 everywhere | Unified; loses JVM-21 features (virtual threads) | |
| Keep template defaults | Zero refactor risk; loses explicit control | |
**User's choice:** JVM 21 everywhere, androidTarget stays JVM 11.
### Q4: Where do library version strings live?
| Option | Description | Selected |
|--------|-------------|----------|
| All versions in libs.versions.toml, nowhere else | Strict INFRA-01 SC#2 | ✓ |
| Catalog for libs, plugin versions inline | Technically violates SC#2 | |
**User's choice:** All versions in libs.versions.toml, nowhere else.
---
## Code-quality toolchain
User clarified: "What are Detekt alternatives? Is ktlint OK?" Discussion explained Detekt = static analysis, ktlint = formatting — not alternatives, usually paired. Presented tiers (minimal / standard / architecture-aware) and user chose minimal.
### Q1: Static analysis (Detekt)?
| Option | Description | Selected |
|--------|-------------|----------|
| Wire Detekt now | Default ruleset + baseline; catches Kotlin footguns | |
| Skip Detekt; lean on IDE + compiler | No CI gate for static analysis | ✓ |
| Placeholder task, no rules | Wire-in-place for future enablement | |
**User's choice:** Skip Detekt. Minimal baseline.
**Notes:** Konsist (architecture fitness) deferred to ~Phase 4 when SyncEngine rules exist.
### Q2: Formatting / linting?
| Option | Description | Selected |
|--------|-------------|----------|
| ktlint via Spotless plugin | One tool: Kotlin + Gradle + markdown | ✓ |
| ktlint plugin directly | Thinner; loses multi-format coverage | |
| Skip, rely on IDE + .editorconfig | No CI-level gate | |
**User's choice:** ktlint via Spotless plugin.
### Q3: Compiler warnings as errors?
| Option | Description | Selected |
|--------|-------------|----------|
| allWarningsAsErrors = true everywhere | Max discipline; deprecations force conscious suppression | ✓ |
| Warn only | Noise accumulates | |
| As-errors for module code, relaxed for generated | Small config carve-out | |
**User's choice:** allWarningsAsErrors = true everywhere.
### Q4: Explicit API mode for shared/?
User clarified: "I don't understand it. What is this explicit api?" and later "Is this some kind of a standard because I am writing kotlin server applications and didn't meet with that". Discussion explained explicitApi as a library-authoring convention (stdlib, coroutines, Ktor etc.) requiring `public` keyword + explicit return types. User weighed the tradeoff and picked strict-on-shared/ on library-contract grounds.
| Option | Description | Selected |
|--------|-------------|----------|
| Skip entirely | Kotlin defaults; no `public` ceremony | |
| Strict on shared/ only | Library discipline on the cross-runtime contract | ✓ |
**User's choice:** Strict on shared/ only.
### Q5: Git hooks?
| Option | Description | Selected |
|--------|-------------|----------|
| No git hooks | `./gradlew check` is the gate; CI later | ✓ |
| Pre-commit hook running spotlessCheck | Blocks commits with formatting drift | |
**User's choice:** No git hooks.
---
## "Running-but-empty" scope
### Q1: Koin DI bootstrap — wire it in Phase 1?
| Option | Description | Selected |
|--------|-------------|----------|
| Wire minimal bootstrap now | Empty appModule + startKoin in App() and MainViewController | ✓ |
| Defer to Phase 2 | Phase 2 does DI + auth together | |
**User's choice:** Wire minimal bootstrap now.
### Q2: Kermit logger bootstrap?
| Option | Description | Selected |
|--------|-------------|----------|
| Set up Kermit now | Logger available from day 1 | ✓ |
| Defer | Add when first feature needs logging | |
**User's choice:** Set up Kermit now.
### Q3: Server "running-but-empty" — `/health` + Flyway scaffold + Postgres config?
| Option | Description | Selected |
|--------|-------------|----------|
| Health endpoint + Flyway scaffold + Postgres conn config | Phase 3 migrations drop into an already-wired migrator | ✓ |
| Health endpoint only, no DB | Phase 3 wires Flyway + Postgres together | |
| Strictly the template skeleton | Most minimal; Phase 2 and 3 do more | |
**User's choice:** Health endpoint + Flyway scaffold + Postgres conn config.
### Q4: docker-compose.yml in Phase 1?
| Option | Description | Selected |
|--------|-------------|----------|
| Add docker-compose.yml now | Phase 3 doesn't have to litigate local-Postgres setup | ✓ |
| Defer to Phase 3 | Compose arrives with first migration | |
| No compose; use homelab Postgres directly | Fastest setup; dev pollutes shared instance | |
**User's choice:** Add docker-compose.yml now.
---
## Claude's Discretion
- Exact ordering of plugin application inside each `build.gradle.kts`
- Specific Spotless ktlint ruleset version (pick latest stable from catalog)
- Whether `application.conf` or a Kotlin config class owns env-var parsing
- Flyway `cleanDisabled` / `baselineOnMigrate` flag choices
- iOS Koin bootstrap idiom (`KoinApplication` vs `startKoin` in MainViewController)
- `docker-compose.yml` shape: `.env` file vs inline localhost defaults
- Exact sentinel JSON body for `/health`
## Deferred Ideas
- Detekt static analysis — revisit only if review misses start compounding
- Konsist architecture fitness tests — revisit ~Phase 4 (SyncEngine rules)
- CI pipeline — Phase 11 (deployment)
- Git hooks — considered; revisit only on recurring format drift
- explicitApi for composeApp / server — rejected (app code, not libraries)
- iosX64 target — rejected (no Intel-Mac contributors)
- `js` target — rejected (wasmJs covers future-web intent)
- Compose Desktop packaging (dmg/msi/exe) — Desktop is dev-only