Compare commits
33 Commits
0ca22f9e36
...
68655eae1a
| Author | SHA1 | Date | |
|---|---|---|---|
| 68655eae1a | |||
| b36058fa79 | |||
| 81bff1db17 | |||
| eaa88fff36 | |||
| fd3e7e1584 | |||
| 129ee616d5 | |||
| 8cd608a981 | |||
| cc5002d1df | |||
| d7ee6b83fc | |||
| 61885455bb | |||
| 6972839fd0 | |||
| c79f9218aa | |||
| 2c786b2fc2 | |||
| f9d3a0c2d4 | |||
| b8671d6dbb | |||
| 59d069591b | |||
| 60221f66a2 | |||
| 37f6191523 | |||
| f691400f2b | |||
| daefe6c26d | |||
| d316a4805e | |||
| 24018efe67 | |||
| 4e6192293f | |||
| 6a69910aa7 | |||
| af4428fd8a | |||
| 7d750af710 | |||
| d76dcea18d | |||
| 4d9aefd4c2 | |||
| aaa8042aee | |||
| d873c31e19 | |||
| b609cb6362 | |||
| 875055a5ef | |||
| 8ef2dbfae4 |
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{kt,kts}]
|
||||||
|
# ktlint configuration for Compose Multiplatform.
|
||||||
|
# - function-naming is disabled because @Composable functions and Kotlin/Native
|
||||||
|
# entry-point factories (e.g. MainViewController) are PascalCase by convention.
|
||||||
|
# - filename is disabled because Compose-Multiplatform entry-point files
|
||||||
|
# (jvmMain/main.kt, webMain/main.kt) follow the Kotlin `fun main()` convention.
|
||||||
|
ktlint_standard_function-naming = disabled
|
||||||
|
ktlint_standard_filename = disabled
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
current_plan: —
|
current_plan: 1
|
||||||
status: Roadmap created; no plan started yet
|
status: executing
|
||||||
last_updated: "2026-04-24T16:07:36.296Z"
|
last_updated: "2026-04-24T17:39:22.205Z"
|
||||||
progress:
|
progress:
|
||||||
total_phases: 11
|
total_phases: 11
|
||||||
completed_phases: 0
|
completed_phases: 0
|
||||||
total_plans: 7
|
total_plans: 7
|
||||||
completed_plans: 0
|
completed_plans: 4
|
||||||
percent: 0
|
percent: 57
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State: Recipe
|
# Project State: Recipe
|
||||||
@@ -25,9 +25,11 @@ progress:
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
**Current focus:** Phase 1: Project Infrastructure & Module Wiring
|
Phase: --phase (01) — EXECUTING
|
||||||
**Current plan:** —
|
Plan: 1 of --name
|
||||||
**Status:** Roadmap created; no plan started yet
|
**Current focus:** Phase --phase — 01
|
||||||
|
**Current plan:** 1
|
||||||
|
**Status:** Executing Phase --phase
|
||||||
**Phase progress:** 0 / 11 phases complete
|
**Phase progress:** 0 / 11 phases complete
|
||||||
**Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0%
|
**Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0%
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
"discuss_mode": "discuss",
|
"discuss_mode": "discuss",
|
||||||
"skip_discuss": false,
|
"skip_discuss": false,
|
||||||
"code_review": true,
|
"code_review": true,
|
||||||
"code_review_depth": "standard"
|
"code_review_depth": "standard",
|
||||||
|
"_auto_chain_active": false
|
||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"context_warnings": true
|
"context_warnings": true
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
phase: 01-project-infrastructure-module-wiring
|
||||||
|
plan: 01
|
||||||
|
subsystem: infra
|
||||||
|
tags: [gradle, version-catalog, kotlin-native, ios-binary-flags, bash, invariants, koin, kermit, flyway, postgresql, spotless, ktor]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires: []
|
||||||
|
provides:
|
||||||
|
- Gradle version catalog extended with Koin (BOM + core/compose/composeViewmodel/android), Kermit, Spotless, Flyway (core + postgresql), Postgres JDBC, Ktor content-negotiation + kotlinx-json serializer
|
||||||
|
- kotlinx-serialization = 1.7.3 version alias (kept even though no library wires it in Plan 01 — Phase 2+ wire Ktor plugins using this pin)
|
||||||
|
- iOS K/N binary flags kotlin.native.binary.gc=cms + kotlin.native.binary.objcDisposeOnMain=false in gradle.properties
|
||||||
|
- tools/verify-no-version-literals.sh (D-09 invariant check)
|
||||||
|
- tools/verify-shared-pure.sh (INFRA-06 / D-19 invariant check — tolerant of pre-scaffold shared/commonMain)
|
||||||
|
- tools/verify-ios-flags.sh (INFRA-03 / D-18 invariant check)
|
||||||
|
affects: [01-02-build-logic, 01-03-module-wiring, 01-04-compose-app, 01-05-server, 01-06-shared, 01-07-validation, 02-auth, 10-ui-chrome, 11-localization-deployment]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added:
|
||||||
|
- Koin 4.2.1 (BOM + 4 consumed modules)
|
||||||
|
- Kermit 2.1.0
|
||||||
|
- Spotless 8.4.0 (plugin)
|
||||||
|
- Flyway 12.4.0 (core + database-postgresql module + Gradle plugin)
|
||||||
|
- PostgreSQL JDBC 42.7.10
|
||||||
|
- Ktor server content-negotiation + kotlinx-json serializer (version.ref = existing ktor 3.4.1)
|
||||||
|
- kotlinx-serialization = 1.7.3 version alias (no library entry yet — pre-wire for ktor serializer which derives its version from ktor)
|
||||||
|
patterns:
|
||||||
|
- "Catalog-only versioning: no numeric version literals in *.gradle.kts outside build-logic/ (D-09 / INFRA-01 SC#2)"
|
||||||
|
- "BOM-managed Koin libs omit version.ref (koin-core/koin-compose/koin-composeViewmodel/koin-android pinned via koin-bom)"
|
||||||
|
- "Fail-loud shell invariants under tools/ — every Phase 1 plan's <automated> block calls one of these three scripts"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- tools/verify-no-version-literals.sh
|
||||||
|
- tools/verify-shared-pure.sh
|
||||||
|
- tools/verify-ios-flags.sh
|
||||||
|
modified:
|
||||||
|
- gradle/libs.versions.toml
|
||||||
|
- gradle.properties
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Refined verify-no-version-literals.sh to exclude top-level project-version assignments (^version = \"x.y.z\") — these are Gradle artifact metadata, not library-version pins. D-09 guards dependencies, not project identity. server/build.gradle.kts:8 keeps its project version."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern 1: All Phase 1+ library/plugin versions declared ONLY in gradle/libs.versions.toml; build scripts reference via libs.* accessors"
|
||||||
|
- "Pattern 2: iOS Kotlin/Native binary flags live in gradle.properties — single file, compiler reads verbatim at link time"
|
||||||
|
- "Pattern 3: Invariant checks as bash scripts under tools/; shebang #!/usr/bin/env bash; set -euo pipefail; fail-loud on violation, silent-pass on clean"
|
||||||
|
- "Pattern 4: Pre-scaffold tolerance — verify-shared-pure.sh exits 0 if shared/src/commonMain doesn't exist (lets invariant scripts run before Plan 07 scaffolds)"
|
||||||
|
|
||||||
|
requirements-completed: [INFRA-01, INFRA-03]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 4min
|
||||||
|
completed: 2026-04-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 01: Foundations Summary
|
||||||
|
|
||||||
|
**Gradle version catalog extended with 6 versions / 11 libraries / 2 plugins (Koin + Kermit + Spotless + Flyway + Postgres + Ktor content-negotiation), iOS K/N binary flags (gc=cms + objcDisposeOnMain=false) added to gradle.properties, and three tools/verify-*.sh invariant scripts shipped — the foundation every remaining Phase 1 plan leans on.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 4 min
|
||||||
|
- **Started:** 2026-04-24T16:12:45Z
|
||||||
|
- **Completed:** 2026-04-24T16:16:53Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 2 (catalog + gradle.properties)
|
||||||
|
- **Files created:** 3 (tools/verify-*.sh)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **Version catalog now covers every Phase 1+ library and plugin**, including BOM-managed Koin modules (omitting `version.ref` by design) and fine-grained Ktor server-side JSON plumbing. No existing version ref was bumped; additive-only per D-09.
|
||||||
|
- **iOS K/N binary flags wired on day 1** (gc=cms + objcDisposeOnMain=false), closing PITFALL #1 before any iOS framework is linked.
|
||||||
|
- **Three invariant scripts ship green** — `verify-ios-flags.sh`, `verify-shared-pure.sh`, `verify-no-version-literals.sh` — exit 0 against current repo state, ready to gate every subsequent plan's automated checks.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task committed atomically on this worktree branch:
|
||||||
|
|
||||||
|
1. **Task 1: Extend gradle/libs.versions.toml with Phase 1 aliases** — `b609cb6` (feat)
|
||||||
|
2. **Task 2: Append iOS K/N binary flags to gradle.properties** — `d873c31` (feat)
|
||||||
|
3. **Task 3: Create verify-*.sh invariant scripts under tools/** — `aaa8042` (feat)
|
||||||
|
|
||||||
|
_No TDD for this plan — all tasks are config/scaffold, not behavior._
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### Created
|
||||||
|
- `tools/verify-no-version-literals.sh` — Grep `*.gradle.kts` for `version = "[0-9]..."`; skip `build-logic/build.gradle.kts` (legitimate plugin-dep literals) and top-level project-version assignments (artifact metadata, not library pins).
|
||||||
|
- `tools/verify-shared-pure.sh` — Grep `shared/src/commonMain/` for imports from `io.ktor`, `androidx.compose`, `org.jetbrains.compose`, `app.cash.sqldelight`. Exits 0 if the directory doesn't exist yet (Plan 07 hasn't scaffolded it).
|
||||||
|
- `tools/verify-ios-flags.sh` — Grep `gradle.properties` for both K/N flags; fail with a clear MISSING: line if either absent.
|
||||||
|
|
||||||
|
### Modified
|
||||||
|
- `gradle/libs.versions.toml` — +6 versions (flyway, kermit, koin, kotlinx-serialization, postgresql, spotless); +11 libraries (5 koin-*, kermit, 2 ktor-server-*, 2 flyway-*, postgresql); +2 plugins (spotless, flywayPlugin). 24 / 33 / 10 totals after edit.
|
||||||
|
- `gradle.properties` — +5 lines (blank separator + comment + comment + 2 K/N flags) appended after existing Android block. Original 10 lines unchanged.
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- **Refined verify-no-version-literals.sh script semantics** — The plan's canonical script (from 01-RESEARCH.md lines 1174–1218 and 01-PATTERNS.md lines 446–490) excluded only `build-logic/build.gradle.kts`. Running it against the current repo tripped on `server/build.gradle.kts:8: version = "1.0.0"` — the Ktor template's project-version property. Per D-09, the invariant targets **library/plugin** version literals, not project/artifact metadata. I added a second, narrow exclusion: lines where the matched `version = "..."` begins at column 0 (unindented project-version assignments). Library and plugin version literals always appear inside a `dependencies { }` or `plugins { }` block and are therefore indented, so they remain caught. Sanity-check: the script still flags a synthetic `dependencies { implementation("x:y") { version = "9.9.9" } }` as a violation.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] Refined verify-no-version-literals.sh to not fire on project-version metadata**
|
||||||
|
- **Found during:** Task 3 (running the script for the first time)
|
||||||
|
- **Issue:** The canonical script in the plan (quoted verbatim from 01-RESEARCH.md) exit-1'd on `server/build.gradle.kts:8: version = "1.0.0"`. That line is the Gradle project-version property (artifact name metadata), not a library-version pin. The plan's acceptance criterion ("`bash tools/verify-no-version-literals.sh` exits 0 today") cannot be satisfied without either removing the project version or refining the script. D-09 (CONTEXT.md line 32) says the rule is "no library versions outside catalog" — project version is out of scope for D-09.
|
||||||
|
- **Fix:** Added a second `grep -v` to exclude lines matching `:[0-9]+:version[[:space:]]*=[[:space:]]*"[0-9]` (unindented top-level project-version). Library/plugin version literals in Gradle DSL are always indented inside a block, so they remain caught. Updated the script's header comment to document the refinement rationale.
|
||||||
|
- **Files modified:** `tools/verify-no-version-literals.sh`
|
||||||
|
- **Verification:** Script exits 0 against current repo state; synthetic indented `version = "9.9.9"` test case still trips the script with exit 1. Both conditions tested.
|
||||||
|
- **Committed in:** `aaa8042` (Task 3 commit — the refined script is the only shipped version; no redundant fix-up commit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (Rule 3 — blocking)
|
||||||
|
**Impact on plan:** Minimal. The refinement strengthens the script's semantic correctness (targets library/plugin pins, not project identity). Success criteria and all acceptance criteria still pass. No additional tasks; no scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
- **Initial worktree base mismatch** — Worktree branch HEAD was `0ca22f9e` (a later commit in the worktree's own history), not the expected `875055a` base. The `<worktree_branch_check>` guard caught it and reset to `875055a` before any work. All three task commits therefore sit cleanly on the required base.
|
||||||
|
- **Planner arithmetic off-by-one** — Plan success criteria say "10 new [libraries] entries"; the plan's own enumeration lists 11 (5 koin + kermit + 2 ktor + 2 flyway + postgresql). I shipped all 11 explicitly named entries. This is a planner-side typo, not a deviation.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None. This plan is pure build configuration — no secrets, no external services, no dashboard config.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- **Plan 02 (build-logic/) unblocked** — `VersionCatalogsExtension.named("libs").findLibrary("koin-core")` etc. will resolve in precompiled plugins; every alias the downstream plans need is now present.
|
||||||
|
- **Plan 03+ module build files unblocked** — modules can reference `libs.koin.core`, `libs.kermit`, `libs.ktor.serverContentNegotiation`, `libs.flyway.core`, `libs.flyway.database.postgresql`, `libs.postgresql` via type-safe accessors.
|
||||||
|
- **Plan 07 (validation) unblocked** — the three `tools/verify-*.sh` scripts are the Wave 0 gate it enumerates.
|
||||||
|
- **No blockers.** `./gradlew build` is NOT expected to pass until Plan 02 wires up `build-logic/` and Plan 03 refactors module build scripts — that's by design and stated in this plan's `<verification>` block.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
Verification of claims in this summary:
|
||||||
|
|
||||||
|
**Created files exist:**
|
||||||
|
- `tools/verify-no-version-literals.sh` — FOUND + executable
|
||||||
|
- `tools/verify-shared-pure.sh` — FOUND + executable
|
||||||
|
- `tools/verify-ios-flags.sh` — FOUND + executable
|
||||||
|
|
||||||
|
**Commits exist in branch history:**
|
||||||
|
- `b609cb6` — FOUND (feat(01-01): extend version catalog with Phase 1 aliases)
|
||||||
|
- `d873c31` — FOUND (feat(01-01): add iOS Kotlin/Native binary flags to gradle.properties)
|
||||||
|
- `aaa8042` — FOUND (feat(01-01): add Phase 1 invariant verification scripts)
|
||||||
|
|
||||||
|
**All three invariant scripts exit 0 against the current repo state.** All success criteria from the plan pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 01-project-infrastructure-module-wiring*
|
||||||
|
*Completed: 2026-04-24*
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
phase: 01-project-infrastructure-module-wiring
|
||||||
|
plan: 02
|
||||||
|
subsystem: infra
|
||||||
|
tags: [gradle, build-logic, included-build, precompiled-plugins, version-catalog, kotlin-multiplatform, compose, ktor, spotless, flyway, pitfall-1, pitfall-2, pitfall-9, pitfall-10]
|
||||||
|
|
||||||
|
requires: [01-01]
|
||||||
|
provides:
|
||||||
|
- "build-logic/ included build resolving the parent catalog via files(\"../gradle/libs.versions.toml\")"
|
||||||
|
- "Precompiled plugin recipe.quality (Spotless + ktlint + D-11 allWarningsAsErrors safety net via plugins.withId guard)"
|
||||||
|
- "Precompiled plugin recipe.kotlin.multiplatform (D-05 target matrix: androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs; JVM toolchain 21 + JVM 11 Android bytecode per D-08; framework baseName = ComposeApp; Koin BOM + koin-core + Kermit + kotlin-test common deps; allWarningsAsErrors at kotlin{} level)"
|
||||||
|
- "Precompiled plugin recipe.compose.multiplatform (layers on recipe.kotlin.multiplatform — PITFALL #2 avoided; Compose + composeCompiler + composeHotReload + commonMain Compose deps + lifecycle-viewmodel-compose + koin-compose)"
|
||||||
|
- "Precompiled plugin recipe.android.application (namespace dev.ulfrx.recipe; findVersion catalog accessor per PITFALL #1; SDK versions from catalog)"
|
||||||
|
- "Precompiled plugin recipe.jvm.server (Kotlin JVM + Ktor + Flyway + application; quoted \"implementation\" configs; cleanDisabled=true; D-08 JVM toolchain 21)"
|
||||||
|
- "Root settings.gradle.kts with includeBuild(\"build-logic\") placed inside pluginManagement{} (PITFALL #9)"
|
||||||
|
- "Root build.gradle.kts with 10 alias(...) apply false entries (8 existing + Spotless + Flyway classloader hints)"
|
||||||
|
affects: [01-03, 01-04, 01-05, 01-06, 01-07]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added:
|
||||||
|
- "build-logic/ included build (kotlin-dsl convention-plugin project)"
|
||||||
|
- "5 precompiled script plugins: recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server"
|
||||||
|
patterns:
|
||||||
|
- "PITFALL #1 mitigation: every precompiled plugin reads versions via extensions.getByType<VersionCatalogsExtension>().named(\"libs\")"
|
||||||
|
- "PITFALL #2 mitigation: recipe.compose.multiplatform applies id(\"recipe.kotlin.multiplatform\") — KMP plugin applied transitively"
|
||||||
|
- "PITFALL #9 mitigation: includeBuild(\"build-logic\") sits inside pluginManagement{}"
|
||||||
|
- "PITFALL #10 mitigation: baseName = \"ComposeApp\" set on both iOS frameworks"
|
||||||
|
- "Quoted-configuration footgun avoidance: recipe.jvm.server uses \"implementation\"(...) string-literal configs"
|
||||||
|
- "D-11 redundancy guard: recipe.quality uses plugins.withId guards for composability"
|
||||||
|
- "Plugin coordinate synthesis via Provider<PluginDependency>.asDependency() keeps build-logic/build.gradle.kts catalog-only"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- build-logic/settings.gradle.kts
|
||||||
|
- build-logic/build.gradle.kts
|
||||||
|
- build-logic/src/main/kotlin/recipe.quality.gradle.kts
|
||||||
|
- build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts
|
||||||
|
- build-logic/src/main/kotlin/recipe.compose.multiplatform.gradle.kts
|
||||||
|
- build-logic/src/main/kotlin/recipe.android.application.gradle.kts
|
||||||
|
- build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
|
||||||
|
modified:
|
||||||
|
- settings.gradle.kts
|
||||||
|
- build.gradle.kts
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Content for all 7 build-logic/ files copied verbatim from 01-RESEARCH.md § Code Examples + 01-PATTERNS.md; no structural changes."
|
||||||
|
- "9 compileOnly(...asDependency()) entries omit androidLibrary — no recipe-family precompiled plugin applies com.android.library; shared/build.gradle.kts applies that plugin directly in 01-03."
|
||||||
|
- "recipe.quality's D-11 safety net is plugins.withId-guarded so the plugin remains composable when applied standalone."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern 1 (role declarations): each recipe.* plugin encodes a module role; shared/ cannot pull Compose transitively (INFRA-06)."
|
||||||
|
- "Pattern 2 (catalog-only versioning inside build-logic): plugin coordinates via asDependency(); library refs via findLibrary; version refs via findVersion.toString().toInt()."
|
||||||
|
- "Pattern 3 (Flyway CLI + runtime split): flyway{} block for CLI ergonomics; runtime migration handled in 01-05."
|
||||||
|
- "Pattern 4 (JVM target split): jvmToolchain(21) drives shared/server/desktop; Android bytecode pinned at JVM 11; server JVM output at JVM 21."
|
||||||
|
|
||||||
|
requirements-completed: [INFRA-02]
|
||||||
|
|
||||||
|
duration: ~5min
|
||||||
|
completed: 2026-04-24
|
||||||
|
tasks-completed: 2
|
||||||
|
files-created: 7
|
||||||
|
files-modified: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 02: build-logic included build + 5 precompiled script plugins
|
||||||
|
|
||||||
|
`build-logic/` scaffolded as an included build whose 5 precompiled script plugins encode D-05/D-06/D-08/D-11/D-20 constraints once, and whose single hook into the root project is `includeBuild("build-logic")` inside `settings.gradle.kts pluginManagement { }` per PITFALL #9.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
- **`build-logic/settings.gradle.kts`** — resolves parent catalog via `from(files("../gradle/libs.versions.toml"))`; `rootProject.name = "build-logic"`.
|
||||||
|
- **`build-logic/build.gradle.kts`** — applies `` `kotlin-dsl` ``; 9 `compileOnly(libs.plugins.*.asDependency())` entries; `Provider<PluginDependency>.asDependency()` extension synthesises coordinates.
|
||||||
|
- **`recipe.quality.gradle.kts`** — Spotless + ktlint on `src/**/*.kt` with `targetExclude("**/build/**", "**/generated/**")`; two `plugins.withId` guards enforce `allWarningsAsErrors.set(true)` on `KotlinCompilationTask<*>` when a Kotlin plugin is present.
|
||||||
|
- **`recipe.kotlin.multiplatform.gradle.kts`** — canonical KMP plugin; `jvmToolchain(21)`; `androidTarget { jvmTarget = JVM_11 }`; `iosArm64()` + `iosSimulatorArm64()` with `baseName = "ComposeApp"; isStatic = true`; `jvm { jvmTarget = JVM_21 }`; `wasmJs { browser() }`; commonMain deps: Koin BOM + koin-core + Kermit; commonTest: kotlin-test.
|
||||||
|
- **`recipe.compose.multiplatform.gradle.kts`** — applies `id("recipe.kotlin.multiplatform")` (PITFALL #2) + compose MP + compose compiler + compose hot-reload; commonMain Compose deps + lifecycle-viewmodel + koin-compose.
|
||||||
|
- **`recipe.android.application.gradle.kts`** — `namespace = "dev.ulfrx.recipe"`; SDK versions via `libs.findVersion("android-compileSdk").get().toString().toInt()` (PITFALL #1); JVM 11 compile options.
|
||||||
|
- **`recipe.jvm.server.gradle.kts`** — Kotlin JVM + Ktor + Flyway + application; `jvmToolchain(21)` + `allWarningsAsErrors.set(true)`; 10 quoted `"implementation"(...)` deps (ktor-server*, logback, flyway-core + flyway-database-postgresql, postgresql JDBC, ktor-serverTestHost, kotlin-testJunit); `flyway{}` block with env-driven URL + `cleanDisabled = true`.
|
||||||
|
- **Root `settings.gradle.kts`** — added `includeBuild("build-logic")` as first statement inside existing `pluginManagement { }`.
|
||||||
|
- **Root `build.gradle.kts`** — appended `alias(libs.plugins.spotless) apply false` and `alias(libs.plugins.flywayPlugin) apply false`. Total apply-false count: 10.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
| Task | Description | Hash |
|
||||||
|
|------|-------------|------|
|
||||||
|
| 1 | Scaffold build-logic/ included build + 5 precompiled plugins | `6a69910` |
|
||||||
|
| 2 | Wire build-logic into root settings.gradle.kts + Spotless/Flyway apply-false | `60221f6` |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — every `<automated>` grep block and acceptance criterion passed first-try.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
This SUMMARY.md was drafted by the executor agent but hook-blocked from being written inside the worktree sandbox; the orchestrator persisted it after merging the worktree into master.
|
||||||
|
|
||||||
|
## Requirements completed
|
||||||
|
|
||||||
|
INFRA-02
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
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*
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
phase: 01-project-infrastructure-module-wiring
|
||||||
|
plan: 04
|
||||||
|
subsystem: client-bootstrap
|
||||||
|
tags: [koin, kermit, di, logging, ios-bridge, android-application, wasm-bootstrap]
|
||||||
|
requires:
|
||||||
|
- 01-02 (build-logic conventions providing Koin + Kermit dependencies via recipe.kotlin.multiplatform)
|
||||||
|
- 01-03 (composeApp/build.gradle.kts wired to convention plugin + libs.koin.android in androidMain)
|
||||||
|
provides:
|
||||||
|
- "initKoin(config: KoinAppDeclaration?): KoinApplication — single bootstrap helper"
|
||||||
|
- "appModule: Koin Module — empty placeholder; Phase 2+ extends with authModule, syncModule, catalogModule"
|
||||||
|
- "configureLogging() — sets Kermit Logger.setTag(\"recipe\")"
|
||||||
|
- "KoinIosKt.doInitKoin() — Swift-callable iOS bridge"
|
||||||
|
- "MainApplication: Android Application subclass invoking configureLogging + initKoin on process boot"
|
||||||
|
affects:
|
||||||
|
- "All future phases (2-11) plug Koin modules into appModule and call Logger.x { } via Kermit"
|
||||||
|
- "Phase 2 (Auth) will register authModule; Phase 4 (SyncEngine) will register syncModule singleton"
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Single initKoin() call site per platform entry point (PITFALL #4 — no double-init on iOS)"
|
||||||
|
- "configureLogging() ALWAYS precedes initKoin() so Koin module loading can use Kermit"
|
||||||
|
- "App.kt (@Composable) NEVER calls startKoin (Pattern 4 anti-pattern guard)"
|
||||||
|
- "iOS Kotlin bridge: top-level fun doInitKoin in KoinIos.kt → Swift symbol KoinIosKt.doInitKoin"
|
||||||
|
- "Wasm init order: configureLogging → initKoin → ComposeViewport (PITFALL #8)"
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
|
||||||
|
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
|
||||||
|
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt
|
||||||
|
- composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt
|
||||||
|
- composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt
|
||||||
|
modified:
|
||||||
|
- composeApp/src/androidMain/AndroidManifest.xml
|
||||||
|
- composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
|
||||||
|
- composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
|
||||||
|
- iosApp/iosApp/iOSApp.swift
|
||||||
|
unchanged_by_design:
|
||||||
|
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt # anti-pattern guard: no startKoin in @Composable
|
||||||
|
- composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt # PITFALL #4: Koin started exclusively in iOSApp.init()
|
||||||
|
- iosApp/iosApp/ContentView.swift # already wraps MainViewControllerKt.MainViewController()
|
||||||
|
decisions:
|
||||||
|
- "Kermit tag = \"recipe\" (D-15) — exact string"
|
||||||
|
- "appModule is empty in Phase 1 (D-14); Phase 2+ adds modules"
|
||||||
|
- "Single iOS Koin call site is iOSApp.init() (PITFALL #4 mitigation)"
|
||||||
|
- "androidContext(this@MainApplication) — qualified `this` because initKoin lambda receiver is KoinApplication"
|
||||||
|
metrics:
|
||||||
|
tasks_completed: 3
|
||||||
|
tasks_total: 3
|
||||||
|
files_created: 5
|
||||||
|
files_modified: 4
|
||||||
|
duration: ~10m
|
||||||
|
completed: 2026-04-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1 Plan 4: Koin + Kermit Bootstrap Wiring — Summary
|
||||||
|
|
||||||
|
Wired the Koin DI container and Kermit structured logger across all four composeApp platform entry points (Android Application subclass, iOS SwiftUI App.init, JVM desktop main, Wasm browser main) with a single `initKoin()` helper in commonMain and an empty `appModule` placeholder that Phase 2+ extends.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
### Task 1 — commonMain DI + logging + iOS bridge (commit `cc5002d`)
|
||||||
|
|
||||||
|
Created four files:
|
||||||
|
|
||||||
|
- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`** — exports `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { config?.invoke(this); modules(appModule) }`. The optional `config` lambda is how Android passes `androidContext(...)` and how Phase 2+ tests can inject overrides without touching the helper itself.
|
||||||
|
- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`** — declares `val appModule = module { }` (empty per D-14). Phase 2 adds `authModule`, Phase 4 adds `syncModule`, etc.
|
||||||
|
- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`** — `fun configureLogging() { Logger.setTag("recipe") }`. Kermit's per-platform writers (OSLog/LogCat/println) install themselves by default; setting the tag is the only required call.
|
||||||
|
- **`composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`** — `fun doInitKoin() { configureLogging(); initKoin() }`. The top-level `fun` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` automatically (Kotlin/Native generates `<FileName>Kt` for top-level decls).
|
||||||
|
|
||||||
|
### Task 2 — Android MainApplication + manifest (commit `8cd608a`)
|
||||||
|
|
||||||
|
- **`composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`** — `class MainApplication : Application()` whose `onCreate()` calls `super.onCreate()`, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }`. Qualified `this@MainApplication` is required because the `initKoin { }` lambda receiver is `KoinApplication`, not the `Application`.
|
||||||
|
- **`composeApp/src/androidMain/AndroidManifest.xml`** — added `android:name=".MainApplication"` as the first attribute on `<application>`. All other attributes and the `<activity>`/`<intent-filter>` subtree preserved verbatim.
|
||||||
|
|
||||||
|
### Task 3 — JVM + Wasm + Swift entry points (commit `fd3e7e1`)
|
||||||
|
|
||||||
|
- **`composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`** — converted `fun main() = application { ... }` (single-expression) into a body block: `configureLogging()` → `initKoin()` → `application { Window(title = "recipe") { App() } }`. Window title and exit handler preserved.
|
||||||
|
- **`composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`** — same init order before `ComposeViewport { App() }`. `@OptIn(ExperimentalComposeUiApi::class)` retained. Defensive against PITFALL #8 (Wasm composition running before DI is ready) — Phase 1 has no ViewModels so the symptom would not surface yet, but the shape is correct from day 1.
|
||||||
|
- **`iosApp/iosApp/iOSApp.swift`** — added `import ComposeApp` (matches framework basename set by `recipe.kotlin.multiplatform`) and `init() { KoinIosKt.doInitKoin() }`. The `WindowGroup { ContentView() }` body is unchanged. `MainViewController.kt` and `ContentView.swift` were intentionally NOT modified — Koin is bootstrapped exclusively from `iOSApp.init()` (PITFALL #4 mitigation).
|
||||||
|
|
||||||
|
## Init order invariant (every platform)
|
||||||
|
|
||||||
|
```
|
||||||
|
configureLogging() → installs Kermit tag "recipe"
|
||||||
|
initKoin() → starts Koin with empty appModule
|
||||||
|
[platform composition entry — application { } / ComposeViewport { } / ComposeUIViewController { } / setContent { }]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written. All 3 tasks completed; all artifacts produced; all `<acceptance_criteria>` satisfied.
|
||||||
|
|
||||||
|
## Confirmations (per `<output>` section of PLAN)
|
||||||
|
|
||||||
|
- Kermit tag = `"recipe"` (D-15) — set in `configureLogging()`.
|
||||||
|
- `appModule` content: empty (D-14) — `val appModule = module { }`.
|
||||||
|
- `App.kt` NOT modified (anti-pattern guard).
|
||||||
|
- `MainViewController.kt` NOT modified (PITFALL #4 guard — Koin started outside).
|
||||||
|
- `ContentView.swift` NOT modified (already wraps `MainViewControllerKt.MainViewController()`).
|
||||||
|
|
||||||
|
## Threat Mitigations Verified
|
||||||
|
|
||||||
|
| Threat ID | Mitigation in delivered code |
|
||||||
|
|-----------|------------------------------|
|
||||||
|
| T-01-04-01 (Koin double-init iOS) | `KoinIosKt.doInitKoin()` is the only init call site on iOS; `MainViewController.kt` does not call `startKoin`. |
|
||||||
|
| T-01-04-02 (Wasm init order) | webMain `main()` orders `configureLogging() → initKoin() → ComposeViewport { }`. |
|
||||||
|
| T-01-04-03 (App.kt calling startKoin) | `App.kt` unchanged; verified no `startKoin` reference outside `Koin.kt`. |
|
||||||
|
|
||||||
|
## Verification gates
|
||||||
|
|
||||||
|
- All three task `<automated>` grep blocks passed.
|
||||||
|
- No build files modified → `tools/verify-no-version-literals.sh` and `tools/verify-shared-pure.sh` remain at exit 0.
|
||||||
|
- Compile gates (`./gradlew build`, `:composeApp:jvmTest`) deferred to Plan 07 per the verification block in 01-04-PLAN.md.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
- `cc5002d` — feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge
|
||||||
|
- `8cd608a` — feat(01-04): add Android MainApplication + manifest registration
|
||||||
|
- `fd3e7e1` — feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
Files verified to exist on disk:
|
||||||
|
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
|
||||||
|
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
|
||||||
|
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt
|
||||||
|
- FOUND: composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt
|
||||||
|
- FOUND: composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt
|
||||||
|
- FOUND: composeApp/src/androidMain/AndroidManifest.xml (modified, contains `android:name=".MainApplication"`)
|
||||||
|
- FOUND: composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (modified)
|
||||||
|
- FOUND: composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (modified)
|
||||||
|
- FOUND: iosApp/iosApp/iOSApp.swift (modified)
|
||||||
|
|
||||||
|
Commits verified in `git log`:
|
||||||
|
- FOUND: cc5002d
|
||||||
|
- FOUND: 8cd608a
|
||||||
|
- FOUND: fd3e7e1
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
phase: 01-project-infrastructure-module-wiring
|
||||||
|
plan: 05
|
||||||
|
subsystem: infra
|
||||||
|
tags: [ktor, flyway, hocon, postgres, slf4j, kotlinx-serialization]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 01-project-infrastructure-module-wiring
|
||||||
|
provides: "recipe.jvm.server precompiled plugin (Plan 02) wires ktor-server-netty, ktor-server-content-negotiation, ktor-serialization-kotlinx-json, flyway-core, flyway-database-postgresql, postgresql JDBC, ktor-server-test-host, logback-classic. Plan 03 applied recipe.jvm.server + recipe.quality to server module and added implementation(projects.shared) so SERVER_PORT is reachable."
|
||||||
|
provides:
|
||||||
|
- "Running-but-empty server: GET /health returns {\"status\":\"ok\"} with Content-Type application/json"
|
||||||
|
- "HOCON application.conf with localhost defaults + ${?ENV} overrides for PORT/DATABASE_URL/DATABASE_USER/DATABASE_PASSWORD"
|
||||||
|
- "Database.migrate() Flyway boot sequence with fail-loud IllegalStateException contract on unreachable Postgres"
|
||||||
|
- "server/src/main/resources/db/migration/ resource directory anchored by .gitkeep so classpath:db/migration resolves before Phase 3 adds V1__init.sql"
|
||||||
|
- "configureRouting() extension extracted from Application.module() so tests compose routing without invoking Database.migrate (no Postgres in CI)"
|
||||||
|
affects: [phase-02-auth, phase-03-households, phase-05-recipe-catalog, phase-11-deployment]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: [Flyway runtime API (flyway-core 12.x), HOCON env-var override pattern, SLF4J server-side logging]
|
||||||
|
patterns:
|
||||||
|
- "HOCON ${?ENV} two-line override pattern (PITFALL #5 mitigation)"
|
||||||
|
- "Fail-loud server boot: Database.migrate throws IllegalStateException on Flyway/JDBC failure"
|
||||||
|
- "Routing extracted to Application.configureRouting() extension so testApplication composes routing without DB dependency"
|
||||||
|
- "Server uses SLF4J/Logback (NOT Kermit — Kermit is client-only)"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- server/src/main/kotlin/dev/ulfrx/recipe/Database.kt
|
||||||
|
- server/src/main/resources/application.conf
|
||||||
|
- server/src/main/resources/db/migration/.gitkeep
|
||||||
|
modified:
|
||||||
|
- server/src/main/kotlin/dev/ulfrx/recipe/Application.kt
|
||||||
|
- server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Use HOCON ${?ENV} optional substitution (two-line default + override) rather than ${ENV:default} (invalid HOCON) or ${ENV} (required, crashes on unset)"
|
||||||
|
- "Server logs via SLF4J/Logback, not Kermit — Kermit reserved for the multiplatform client"
|
||||||
|
- "Database.migrate is fail-loud: IllegalStateException on any Flyway error; no silent degraded mode"
|
||||||
|
- "cleanDisabled(true) is double-enforced (precompiled plugin CLI guard + programmatic Database.migrate guard)"
|
||||||
|
- "Extract Application.configureRouting() so /health test runs without Postgres — preserves D-11 invariant that ./gradlew :server:test passes in fresh clones / CI"
|
||||||
|
- "Default credentials in application.conf (recipe/recipe/recipe @ localhost:5432/recipe) match Plan 06 docker-compose for zero-config dev boot"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "HOCON ${?ENV} override: every secret/per-env value gets a default line followed by ${?ENV_VAR} optional substitution"
|
||||||
|
- "Fail-loud infrastructure: critical boot operations (DB migration, future JWKS load) throw IllegalStateException rather than returning a status"
|
||||||
|
- "Routing extraction for testability: features expose Application.configureXxx() extensions; module() is the production composition root"
|
||||||
|
|
||||||
|
requirements-completed: [INFRA-02]
|
||||||
|
|
||||||
|
duration: ~1 min (executor work — implementation commits authored ahead of executor invocation)
|
||||||
|
completed: 2026-04-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 05: Server /health + Flyway + HOCON Boot Summary
|
||||||
|
|
||||||
|
**Running-but-empty Ktor server: HOCON-configured Flyway boot with fail-loud Postgres contract, GET /health returning `{"status":"ok"}`, and a routing extraction that lets tests verify the route without a running database.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** Implementation commits span 2026-04-24 18:22:08 → 18:23:14 (~66s of authoring); executor verification + SUMMARY ~1 min
|
||||||
|
- **Started:** 2026-04-24T18:22:08Z (commit 24018ef)
|
||||||
|
- **Completed:** 2026-04-24T18:23:14Z (commit 59d0695)
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 5 (3 created, 2 modified)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- HOCON `application.conf` reads PORT + DATABASE_URL/USER/PASSWORD via the `${?ENV}` two-line override pattern; defaults match the Plan 06 docker-compose stack so `docker compose up -d postgres && ./gradlew :server:run` works with zero env config.
|
||||||
|
- `Database.migrate(app: Application)` runs `Flyway.configure().dataSource(...).locations("classpath:db/migration").baselineOnMigrate(true).validateOnMigrate(true).cleanDisabled(true).load().migrate()` and throws `IllegalStateException` on any failure — D-16 fail-loud contract satisfied.
|
||||||
|
- `db/migration/.gitkeep` keeps the resource directory in the repo so Flyway's classpath resolution succeeds before Phase 3 introduces the first SQL migration.
|
||||||
|
- `Application.kt` rewritten with explicit Ktor imports (D-11 allWarningsAsErrors clean), installs `ContentNegotiation { json() }`, calls `Database.migrate(this)`, then delegates to `Application.configureRouting()` which exposes `GET /health → Health(status="ok")`.
|
||||||
|
- `ApplicationTest.kt` rewritten to compose `configureRouting()` directly (skipping `Database.migrate`) so `./gradlew :server:test --tests "*health*"` passes without a running Postgres — required for fresh-clone / CI runs.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically prior to executor invocation (commits already in branch history):
|
||||||
|
|
||||||
|
1. **Task 1: HOCON config + db/migration/.gitkeep + Database.kt** — `24018ef` (feat)
|
||||||
|
2. **Task 2: Application.kt rewrite (ContentNegotiation, Flyway boot, /health)** — `daefe6c` (refactor)
|
||||||
|
3. **Task 3: ApplicationTest.kt rewrite (no-Postgres /health assertion)** — `59d0695` (test)
|
||||||
|
|
||||||
|
**Plan metadata:** appended in this commit (docs).
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `server/src/main/resources/application.conf` (created) — HOCON config: ktor.deployment.port + database.{url,user,password} with `${?ENV}` overrides
|
||||||
|
- `server/src/main/resources/db/migration/.gitkeep` (created) — anchors the Flyway classpath resource directory in git
|
||||||
|
- `server/src/main/kotlin/dev/ulfrx/recipe/Database.kt` (created) — `object Database { fun migrate(app) }` with fail-loud Flyway invocation, SLF4J logging
|
||||||
|
- `server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` (modified) — explicit imports; installs ContentNegotiation; runs Database.migrate; delegates to configureRouting(); exposes GET /health returning serializable `Health(status)`
|
||||||
|
- `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` (modified) — replaces template `testRoot()` with health-endpoint test that composes routing without DB
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
See `key-decisions` in frontmatter. Highlights:
|
||||||
|
|
||||||
|
- HOCON `${?ENV}` optional substitution chosen over `${ENV}` (required) and `${ENV:default}` (invalid HOCON) per PITFALL #5.
|
||||||
|
- Server logging via SLF4J/Logback (not Kermit) because Logback is already wired in `recipe.jvm.server` and Kermit is reserved for the multiplatform client.
|
||||||
|
- `Application.configureRouting()` extension extracted to satisfy the no-Postgres-required invariant for `./gradlew :server:test`.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written. All artifacts match the plan's `must_haves` (truths, artifacts, key_links) verified against the filesystem; explicit imports satisfy D-11; `${?ENV}` lines all present; fail-loud contract intact; `Database.migrate` not referenced from the test.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None — no external service configuration required. Postgres for end-to-end boot is provided by the Plan 06 docker-compose stack; Plan 05's own success criteria (test passing without a running DB) require nothing from the operator.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Phase 2 (Auth) inherits a Ktor server with ContentNegotiation pre-installed, so JWT validation routes can return `@Serializable` DTOs immediately.
|
||||||
|
- Phase 3 (Households) drops `V1__init.sql` into `server/src/main/resources/db/migration/`; the Flyway boot pathway is already validated.
|
||||||
|
- Phase 11 (Deployment) inherits the HOCON `${?ENV}` pattern; homelab deploy configures `DATABASE_URL/USER/PASSWORD` via env vars without touching `application.conf`.
|
||||||
|
- Manual end-to-end verification (`docker compose up -d postgres && ./gradlew :server:run && curl http://localhost:8080/health`) deferred to Plan 07 / manual smoke per the plan's verification section.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- File `server/src/main/resources/application.conf` — FOUND
|
||||||
|
- File `server/src/main/resources/db/migration/.gitkeep` — FOUND
|
||||||
|
- File `server/src/main/kotlin/dev/ulfrx/recipe/Database.kt` — FOUND
|
||||||
|
- File `server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` — FOUND
|
||||||
|
- File `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` — FOUND
|
||||||
|
- Commit `24018ef` (feat 01-05 Task 1) — FOUND in git log
|
||||||
|
- Commit `daefe6c` (refactor 01-05 Task 2) — FOUND in git log
|
||||||
|
- Commit `59d0695` (test 01-05 Task 3) — FOUND in git log
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-project-infrastructure-module-wiring*
|
||||||
|
*Completed: 2026-04-24*
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
phase: 01-project-infrastructure-module-wiring
|
||||||
|
plan: 06
|
||||||
|
subsystem: dev-ergonomics
|
||||||
|
tags: [docker-compose, postgres, readme, local-dev, infra]
|
||||||
|
dependency_graph:
|
||||||
|
requires: []
|
||||||
|
provides:
|
||||||
|
- "Local Postgres 16 dev instance matching application.conf HOCON defaults (recipe/recipe/recipe)"
|
||||||
|
- "Named volume recipe-pgdata for persistence across container restarts"
|
||||||
|
- "pg_isready healthcheck enabling docker compose up --wait usage"
|
||||||
|
- "README 'Local development' section documenting the two-command dev loop"
|
||||||
|
affects:
|
||||||
|
- "server/src/main/resources/application.conf (Plan 05 — credentials match contract)"
|
||||||
|
- "Phase 3 (Households + DB migrations) — depends on a working local Postgres"
|
||||||
|
- "Phase 11 (homelab deployment) — separate compose config will diverge from this dev-local one"
|
||||||
|
tech_stack:
|
||||||
|
added:
|
||||||
|
- "postgres:16 (Docker image, pinned major version)"
|
||||||
|
patterns:
|
||||||
|
- "Dev-local compose file committed to repo (non-secret literal creds)"
|
||||||
|
- "Healthcheck via pg_isready gating sequencing"
|
||||||
|
- "Named Docker volume for data persistence"
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- "docker-compose.yml"
|
||||||
|
modified:
|
||||||
|
- "README.md"
|
||||||
|
decisions:
|
||||||
|
- "Kept it single-service: postgres only. Authentik stays on homelab (CONTEXT.md D-17); Ktor server runs via Gradle on the dev host for fast iteration."
|
||||||
|
- "Pinned postgres:16 (not :latest, not :15) matching D-17 scope statement."
|
||||||
|
- "No version: key in compose file — modern docker compose v2 treats it as legacy and emits warnings."
|
||||||
|
- "No .env file in this plan — inline POSTGRES_* is fine for single-dev + matching application.conf defaults (D-17 / PATTERNS.md recommendation)."
|
||||||
|
- "Port binding 5432:5432 is dev-local; README calls it out. Phase 11 homelab compose will use a different approach."
|
||||||
|
metrics:
|
||||||
|
duration_seconds: 92
|
||||||
|
duration_human: "1m32s"
|
||||||
|
tasks_completed: 2
|
||||||
|
files_created: 1
|
||||||
|
files_modified: 1
|
||||||
|
completed_at: "2026-04-24T16:22:48Z"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01 Plan 06: Dev ergonomics — docker-compose + README Local development summary
|
||||||
|
|
||||||
|
Shipped `docker-compose.yml` (single postgres:16 service, named volume, healthcheck — credentials matching Plan 05's `application.conf` HOCON defaults exactly) and a "Local development" README section documenting the `docker compose up -d postgres && ./gradlew :server:run && curl /health` dev loop, while dropping the legacy `js` target docs per D-01.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
### docker-compose.yml (20 lines)
|
||||||
|
|
||||||
|
- `services.postgres`:
|
||||||
|
- `image: postgres:16` (pinned major version)
|
||||||
|
- `container_name: recipe-postgres`
|
||||||
|
- `environment`: `POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD` all literal `recipe`
|
||||||
|
- `ports: "5432:5432"` (dev-local loopback via host Docker)
|
||||||
|
- `volumes: recipe-pgdata:/var/lib/postgresql/data` (persistence)
|
||||||
|
- `healthcheck`: `pg_isready -U recipe -d recipe` every 5s, timeout 5s, 5 retries
|
||||||
|
- Top-level `volumes.recipe-pgdata:` (named volume declaration)
|
||||||
|
- No `version:` key (modern compose v2)
|
||||||
|
- No additional services (no Authentik — lives on user's homelab per D-17)
|
||||||
|
|
||||||
|
### README.md edits
|
||||||
|
|
||||||
|
**Edit A — dropped js target block** (lines 77-85 of previous README): the "- for the JS target (slower, supports older browsers)" paragraph and its two command blocks were deleted. The `wasmJs` paragraph is preserved intact.
|
||||||
|
|
||||||
|
**Edit B — inserted new "Local development" section** (after the iOS subsection, before the trailing `---` horizontal rule):
|
||||||
|
|
||||||
|
- Two-command boot: `docker compose up -d postgres` + `./gradlew :server:run`
|
||||||
|
- Smoke test: `curl http://localhost:8080/health` with expected `{"status":"ok"}` response
|
||||||
|
- Documented env-var overrides: `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD`, `PORT`
|
||||||
|
- Pre-commit formatter hint: `./gradlew spotlessApply` (D-10)
|
||||||
|
- Full-suite: `./gradlew check`
|
||||||
|
- DB reset: `docker compose down -v` (destroys `recipe-pgdata`)
|
||||||
|
|
||||||
|
All other existing headings (Android, Desktop/JVM, Server, iOS, web `wasmJs`) and the top introduction (lines 1-20) are unchanged. The trailing `---` + learn-more links paragraph is unchanged.
|
||||||
|
|
||||||
|
## Credential-match contract with Plan 05
|
||||||
|
|
||||||
|
The three compose env-vars are byte-identical to the literals in `server/src/main/resources/application.conf`:
|
||||||
|
|
||||||
|
| compose env | application.conf |
|
||||||
|
|-------------|------------------|
|
||||||
|
| `POSTGRES_DB: recipe` | JDBC URL path `/recipe` |
|
||||||
|
| `POSTGRES_USER: recipe` | `user = "recipe"` |
|
||||||
|
| `POSTGRES_PASSWORD: recipe` | `password = "recipe"` |
|
||||||
|
|
||||||
|
Verified via `grep -c '^\s*POSTGRES_\(DB\|USER\|PASSWORD\): recipe$' docker-compose.yml` → `3`.
|
||||||
|
|
||||||
|
## Requirements addressed
|
||||||
|
|
||||||
|
- **INFRA-02** — local development environment via `docker-compose.yml` and README dev loop documentation.
|
||||||
|
|
||||||
|
## Tasks executed
|
||||||
|
|
||||||
|
| Task | Name | Commit | Files |
|
||||||
|
|------|------|--------|-------|
|
||||||
|
| 1 | Create docker-compose.yml at repo root | `af4428f` | docker-compose.yml (new) |
|
||||||
|
| 2 | Add "Local development" section to README.md and drop js target docs | `f691400` | README.md (modified) |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written. No Rule 1-3 auto-fixes, no checkpoints, no auth gates. Both `<automated>` verify blocks and every acceptance criterion passed on first attempt.
|
||||||
|
|
||||||
|
## Threat surface scan
|
||||||
|
|
||||||
|
No new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries were introduced beyond what the plan's `<threat_model>` already covers (T-01-06-01..04). The `5432:5432` host binding and literal `recipe/recipe/recipe` credentials are the exact surface the plan's STRIDE register dispositions (`mitigate`/`accept`) already cover. No new flags.
|
||||||
|
|
||||||
|
## Known stubs
|
||||||
|
|
||||||
|
None. Both deliverables are complete — no placeholders, no TODOs, no empty data paths.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
**Task 1 automated check:**
|
||||||
|
```
|
||||||
|
test -f docker-compose.yml && grep -q 'image: postgres:16' ... && grep -q 'pg_isready -U recipe -d recipe' ... && grep -q '^volumes:$' ...
|
||||||
|
→ VERIFY PASS
|
||||||
|
grep -c '^\s*POSTGRES_\(DB\|USER\|PASSWORD\): recipe$' docker-compose.yml → 3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task 2 automated check:**
|
||||||
|
```
|
||||||
|
grep -q 'Local development' && grep -q 'docker compose up -d postgres' && grep -q 'curl http://localhost:8080/health' && grep -q 'DATABASE_URL' && grep -q 'gradlew spotlessApply' && grep -q 'docker compose down -v' && ! grep -q 'jsBrowserDevelopmentRun' && grep -q 'wasmJsBrowserDevelopmentRun'
|
||||||
|
→ VERIFY PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Acceptance criteria — Task 2 individually confirmed:**
|
||||||
|
- `Local development` appears exactly once (section heading)
|
||||||
|
- All 4 env-vars listed: `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD`, `PORT`
|
||||||
|
- `gradlew check` present
|
||||||
|
- Existing section headings (Android / Desktop (JVM) / Server / iOS) all preserved (grep `-c` → `1` each)
|
||||||
|
- `jsBrowserDevelopmentRun` absent; `wasmJsBrowserDevelopmentRun` present
|
||||||
|
- Top introduction (lines 1-20) unchanged
|
||||||
|
|
||||||
|
## Manual sanity checks (optional, not blocking)
|
||||||
|
|
||||||
|
Skipped per plan `<verification>`:
|
||||||
|
- `docker compose config` YAML parse — not blocking per plan; docker may not be running in this worktree sandbox.
|
||||||
|
- `docker compose up -d postgres && pg_isready` live test — not required; will be validated in Phase 3 when migrations land.
|
||||||
|
|
||||||
|
## Notes for downstream plans
|
||||||
|
|
||||||
|
- **Plan 05** (this wave) — credential contract lives in both files; any future change to the `recipe/recipe/recipe` triple MUST update both `application.conf` AND `docker-compose.yml` in the same commit.
|
||||||
|
- **Phase 3** (Households + DB migrations) — can add `depends_on: { postgres: { condition: service_healthy } }` to a future `server` service in compose if we ever run the Ktor server in Docker; the healthcheck is already wired for it.
|
||||||
|
- **Phase 11** (homelab deployment) — will ship a separate compose file (not editing this one) because homelab creds are secret and this file's creds are deliberately non-secret literals.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `docker-compose.yml` exists at repo root: FOUND
|
||||||
|
- `README.md` contains "Local development" section: FOUND
|
||||||
|
- Commit `af4428f` (Task 1): FOUND in `git log`
|
||||||
|
- Commit `f691400` (Task 2): FOUND in `git log`
|
||||||
|
- All acceptance criteria from both tasks verified via grep
|
||||||
|
- No file deletions in either commit
|
||||||
53
README.md
53
README.md
@@ -74,21 +74,56 @@ in your IDE's toolbar or run it directly from the terminal:
|
|||||||
```shell
|
```shell
|
||||||
.\gradlew.bat :composeApp:wasmJsBrowserDevelopmentRun
|
.\gradlew.bat :composeApp:wasmJsBrowserDevelopmentRun
|
||||||
```
|
```
|
||||||
- for the JS target (slower, supports older browsers):
|
|
||||||
- on macOS/Linux
|
|
||||||
```shell
|
|
||||||
./gradlew :composeApp:jsBrowserDevelopmentRun
|
|
||||||
```
|
|
||||||
- on Windows
|
|
||||||
```shell
|
|
||||||
.\gradlew.bat :composeApp:jsBrowserDevelopmentRun
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build and Run iOS Application
|
### Build and Run iOS Application
|
||||||
|
|
||||||
To build and run the development version of the iOS app, use the run configuration from the run widget
|
To build and run the development version of the iOS app, use the run configuration from the run widget
|
||||||
in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there.
|
in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there.
|
||||||
|
|
||||||
|
### Local development
|
||||||
|
|
||||||
|
The server requires Postgres. A `docker-compose.yml` at the repo root ships a local Postgres
|
||||||
|
instance whose credentials match `application.conf` defaults (`recipe`/`recipe`/`recipe`).
|
||||||
|
|
||||||
|
Boot the database and server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker compose up -d postgres
|
||||||
|
./gradlew :server:run
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the server is up:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
# expected: {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment overrides (optional — set any of these to override `application.conf` defaults):
|
||||||
|
|
||||||
|
- `DATABASE_URL` — JDBC URL (default `jdbc:postgresql://localhost:5432/recipe`)
|
||||||
|
- `DATABASE_USER` — DB user (default `recipe`)
|
||||||
|
- `DATABASE_PASSWORD` — DB password (default `recipe`)
|
||||||
|
- `PORT` — Ktor port (default `8080`)
|
||||||
|
|
||||||
|
Before committing, format all Kotlin + Gradle + Markdown files:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew spotlessApply
|
||||||
|
```
|
||||||
|
|
||||||
|
The full check (Spotless + all tests across all targets):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew check
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset the local database (destroys the `recipe-pgdata` volume):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html),
|
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html),
|
||||||
|
|||||||
18
build-logic/build.gradle.kts
Normal file
18
build-logic/build.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.plugins.kotlinMultiplatform.asDependency())
|
||||||
|
compileOnly(libs.plugins.androidApplication.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}" }
|
||||||
14
build-logic/settings.gradle.kts
Normal file
14
build-logic/settings.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "build-logic"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
Normal file
41
build-logic/src/main/kotlin/recipe.jvm.server.gradle.kts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relax allWarningsAsErrors for KLIB-merging metadata tasks. KotlinCompileCommon
|
||||||
|
// aggregates dependency KLIBs and surfaces upstream "duplicated unique_name"
|
||||||
|
// resolver warnings caused by androidx.lifecycle 2.10.0 (Android-only) and
|
||||||
|
// org.jetbrains.androidx.lifecycle 2.10.0 (CMP) co-publishing artifacts with
|
||||||
|
// matching KLIB unique_names. This is an upstream Compose-Multiplatform 1.10 +
|
||||||
|
// lifecycle 2.10 ecosystem condition (KT-62515-style), not actionable in our
|
||||||
|
// source — so we keep -Werror on real source compilation tasks but disable it
|
||||||
|
// for the metadata-aggregation step where no user code is being compiled.
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
allWarningsAsErrors.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
build-logic/src/main/kotlin/recipe.quality.gradle.kts
Normal file
40
build-logic/src/main/kotlin/recipe.quality.gradle.kts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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 alongside a Kotlin plugin
|
||||||
|
// (multiplatform or jvm), ensure allWarningsAsErrors still applies even if the module
|
||||||
|
// build didn't already configure it. Guarded with plugins.withId so this plugin is
|
||||||
|
// safely composable even when applied alone (no KotlinCompilationTask type available
|
||||||
|
// on the classpath until a Kotlin plugin is present).
|
||||||
|
plugins.withId("org.jetbrains.kotlin.multiplatform") {
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
allWarningsAsErrors.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins.withId("org.jetbrains.kotlin.jvm") {
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
allWarningsAsErrors.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,6 @@ plugins {
|
|||||||
alias(libs.plugins.kotlinJvm) apply false
|
alias(libs.plugins.kotlinJvm) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.ktor) apply false
|
alias(libs.plugins.ktor) apply false
|
||||||
|
alias(libs.plugins.spotless) apply false
|
||||||
|
alias(libs.plugins.flywayPlugin) apply false
|
||||||
}
|
}
|
||||||
@@ -1,68 +1,24 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
// AGP must apply BEFORE recipe.kotlin.multiplatform — the latter calls androidTarget(),
|
||||||
alias(libs.plugins.androidApplication)
|
// which requires the Android Gradle Plugin to already be on the project. Gradle applies
|
||||||
alias(libs.plugins.composeMultiplatform)
|
// plugin IDs in declaration order, so recipe.android.application is listed first.
|
||||||
alias(libs.plugins.composeCompiler)
|
id("recipe.android.application")
|
||||||
alias(libs.plugins.composeHotReload)
|
id("recipe.kotlin.multiplatform")
|
||||||
|
id("recipe.compose.multiplatform")
|
||||||
|
id("recipe.quality")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
js {
|
|
||||||
browser()
|
|
||||||
binaries.executable()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
|
||||||
browser()
|
|
||||||
binaries.executable()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.koin.android)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.runtime)
|
|
||||||
implementation(libs.compose.foundation)
|
|
||||||
implementation(libs.compose.material3)
|
|
||||||
implementation(libs.compose.ui)
|
|
||||||
implementation(libs.compose.components.resources)
|
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
|
||||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
|
||||||
implementation(libs.kotlin.test)
|
|
||||||
}
|
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(libs.kotlinx.coroutinesSwing)
|
implementation(libs.kotlinx.coroutinesSwing)
|
||||||
@@ -70,45 +26,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "dev.ulfrx.recipe"
|
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "dev.ulfrx.recipe"
|
|
||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
|
||||||
targetSdk = libs.versions.android.targetSdk.get().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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
debugImplementation(libs.compose.uiTooling)
|
debugImplementation(libs.compose.uiTooling)
|
||||||
}
|
}
|
||||||
|
|
||||||
compose.desktop {
|
|
||||||
application {
|
|
||||||
mainClass = "dev.ulfrx.recipe.MainKt"
|
|
||||||
|
|
||||||
nativeDistributions {
|
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
|
||||||
packageName = "dev.ulfrx.recipe"
|
|
||||||
packageVersion = "1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dev.ulfrx.recipe.di.initKoin
|
||||||
|
import dev.ulfrx.recipe.logging.configureLogging
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
|
||||||
|
class MainApplication : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
configureLogging()
|
||||||
|
initKoin {
|
||||||
|
androidContext(this@MainApplication)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,15 @@ import androidx.compose.foundation.layout.safeContentPadding
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
import recipe.composeapp.generated.resources.compose_multiplatform
|
import recipe.composeapp.generated.resources.compose_multiplatform
|
||||||
|
|
||||||
@@ -25,7 +28,8 @@ fun App() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
var showContent by remember { mutableStateOf(false) }
|
var showContent by remember { mutableStateOf(false) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
.safeContentPadding()
|
.safeContentPadding()
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.ulfrx.recipe.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
// Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
|
||||||
|
val appModule =
|
||||||
|
module {
|
||||||
|
// intentionally empty in Phase 1
|
||||||
|
}
|
||||||
11
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
Normal file
11
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.ulfrx.recipe.di
|
||||||
|
|
||||||
|
import org.koin.core.KoinApplication
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
|
|
||||||
|
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication =
|
||||||
|
startKoin {
|
||||||
|
config?.invoke(this)
|
||||||
|
modules(appModule)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.ulfrx.recipe.logging
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
|
||||||
|
fun configureLogging() {
|
||||||
|
Logger.setTag("recipe")
|
||||||
|
// Platform log writers (OSLog iOS, LogCat Android, System.out JVM/Wasm) install by default.
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class ComposeAppCommonTest {
|
class ComposeAppCommonTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun example() {
|
fun example() {
|
||||||
assertEquals(3, 1 + 2)
|
assertEquals(3, 1 + 2)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.ulfrx.recipe.di
|
||||||
|
|
||||||
|
import dev.ulfrx.recipe.logging.configureLogging
|
||||||
|
|
||||||
|
fun doInitKoin() {
|
||||||
|
configureLogging()
|
||||||
|
initKoin()
|
||||||
|
}
|
||||||
@@ -2,12 +2,18 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
|
import dev.ulfrx.recipe.di.initKoin
|
||||||
|
import dev.ulfrx.recipe.logging.configureLogging
|
||||||
|
|
||||||
fun main() = application {
|
fun main() {
|
||||||
|
configureLogging()
|
||||||
|
initKoin()
|
||||||
|
application {
|
||||||
Window(
|
Window(
|
||||||
onCloseRequest = ::exitApplication,
|
onCloseRequest = ::exitApplication,
|
||||||
title = "recipe",
|
title = "recipe",
|
||||||
) {
|
) {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,13 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.window.ComposeViewport
|
import androidx.compose.ui.window.ComposeViewport
|
||||||
|
import dev.ulfrx.recipe.di.initKoin
|
||||||
|
import dev.ulfrx.recipe.logging.configureLogging
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
fun main() {
|
fun main() {
|
||||||
|
configureLogging()
|
||||||
|
initKoin()
|
||||||
ComposeViewport {
|
ComposeViewport {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
|
|||||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: recipe-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: recipe
|
||||||
|
POSTGRES_USER: recipe
|
||||||
|
POSTGRES_PASSWORD: recipe
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- recipe-pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U recipe -d recipe"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
recipe-pgdata:
|
||||||
@@ -8,3 +8,8 @@ org.gradle.caching=true
|
|||||||
#Android
|
#Android
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Kotlin/Native iOS (PITFALLS.md #1; D-18; INFRA-03) — MANDATORY day 1
|
||||||
|
# CMS GC + non-main-thread Obj-C deinit to avoid UI-thread pause spikes in Compose Multiplatform.
|
||||||
|
kotlin.native.binary.gc=cms
|
||||||
|
kotlin.native.binary.objcDisposeOnMain=false
|
||||||
@@ -11,12 +11,18 @@ androidx-lifecycle = "2.10.0"
|
|||||||
androidx-testExt = "1.3.0"
|
androidx-testExt = "1.3.0"
|
||||||
composeHotReload = "1.0.0"
|
composeHotReload = "1.0.0"
|
||||||
composeMultiplatform = "1.10.3"
|
composeMultiplatform = "1.10.3"
|
||||||
|
flyway = "12.4.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
kermit = "2.1.0"
|
||||||
|
koin = "4.2.1"
|
||||||
kotlin = "2.3.20"
|
kotlin = "2.3.20"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
|
kotlinx-serialization = "1.7.3"
|
||||||
ktor = "3.4.1"
|
ktor = "3.4.1"
|
||||||
logback = "1.5.32"
|
logback = "1.5.32"
|
||||||
material3 = "1.10.0-alpha05"
|
material3 = "1.10.0-alpha05"
|
||||||
|
postgresql = "42.7.10"
|
||||||
|
spotless = "8.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
@@ -42,6 +48,23 @@ ktor-serverCore = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor
|
|||||||
ktor-serverNetty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
ktor-serverNetty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
||||||
ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" }
|
ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" }
|
||||||
|
|
||||||
|
# Koin (client DI — D-14)
|
||||||
|
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
|
||||||
|
koin-core = { module = "io.insert-koin:koin-core" }
|
||||||
|
koin-compose = { module = "io.insert-koin:koin-compose" }
|
||||||
|
koin-composeViewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
|
||||||
|
koin-android = { module = "io.insert-koin:koin-android" }
|
||||||
|
|
||||||
|
# Kermit (client logger — D-15)
|
||||||
|
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
||||||
|
|
||||||
|
# Server: Ktor content-negotiation + JSON serializer + Flyway + Postgres (D-16)
|
||||||
|
ktor-serverContentNegotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" }
|
||||||
|
ktor-serializationKotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" }
|
||||||
|
flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" }
|
||||||
|
flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" }
|
||||||
|
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||||
@@ -51,3 +74,5 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
|
|||||||
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
|
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
|
||||||
|
flywayPlugin = { id = "org.flywaydb.flyway", version.ref = "flyway" }
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct iOSApp: App {
|
struct iOSApp: App {
|
||||||
|
init() {
|
||||||
|
KoinIosKt.doInitKoin()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
id("recipe.jvm.server")
|
||||||
alias(libs.plugins.ktor)
|
id("recipe.quality")
|
||||||
application
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.ulfrx.recipe"
|
group = "dev.ulfrx.recipe"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("dev.ulfrx.recipe.ApplicationKt")
|
mainClass.set("dev.ulfrx.recipe.ApplicationKt")
|
||||||
|
|
||||||
@@ -15,9 +15,4 @@ application {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
implementation(libs.logback)
|
|
||||||
implementation(libs.ktor.serverCore)
|
|
||||||
implementation(libs.ktor.serverNetty)
|
|
||||||
testImplementation(libs.ktor.serverTestHost)
|
|
||||||
testImplementation(libs.kotlin.testJunit)
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.netty.Netty
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module)
|
embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module)
|
||||||
.start(wait = true)
|
.start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class Health(
|
||||||
|
val status: String,
|
||||||
|
)
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
Database.migrate(this)
|
||||||
|
configureRouting()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.configureRouting() {
|
||||||
routing {
|
routing {
|
||||||
get("/") {
|
get("/health") {
|
||||||
call.respondText("Ktor: ${Greeting().greet()}")
|
call.respond(Health(status = "ok"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
41
server/src/main/kotlin/dev/ulfrx/recipe/Database.kt
Normal file
41
server/src/main/kotlin/dev/ulfrx/recipe/Database.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
|
|
||||||
|
fun migrate(app: Application) {
|
||||||
|
val url =
|
||||||
|
app.environment.config
|
||||||
|
.property("database.url")
|
||||||
|
.getString()
|
||||||
|
val user =
|
||||||
|
app.environment.config
|
||||||
|
.property("database.user")
|
||||||
|
.getString()
|
||||||
|
val password =
|
||||||
|
app.environment.config
|
||||||
|
.property("database.password")
|
||||||
|
.getString()
|
||||||
|
|
||||||
|
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
Flyway
|
||||||
|
.configure()
|
||||||
|
.dataSource(url, user, password)
|
||||||
|
.locations("classpath:db/migration")
|
||||||
|
.baselineOnMigrate(true)
|
||||||
|
.validateOnMigrate(true)
|
||||||
|
.cleanDisabled(true)
|
||||||
|
.load()
|
||||||
|
.migrate()
|
||||||
|
}.onFailure { ex ->
|
||||||
|
log.error("Flyway migration failed — cannot start server", ex)
|
||||||
|
throw IllegalStateException("Database unreachable or migration failed", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
server/src/main/resources/application.conf
Normal file
18
server/src/main/resources/application.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
ktor {
|
||||||
|
deployment {
|
||||||
|
port = 8080
|
||||||
|
port = ${?PORT}
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
modules = [ dev.ulfrx.recipe.ApplicationKt.module ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
url = "jdbc:postgresql://localhost:5432/recipe"
|
||||||
|
url = ${?DATABASE_URL}
|
||||||
|
user = "recipe"
|
||||||
|
user = ${?DATABASE_USER}
|
||||||
|
password = "recipe"
|
||||||
|
password = ${?DATABASE_PASSWORD}
|
||||||
|
}
|
||||||
0
server/src/main/resources/db/migration/.gitkeep
Normal file
0
server/src/main/resources/db/migration/.gitkeep
Normal file
@@ -1,20 +1,30 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.*
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.testing.*
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlin.test.*
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ApplicationTest {
|
class ApplicationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRoot() = testApplication {
|
fun `health endpoint returns 200 with status ok`() =
|
||||||
|
testApplication {
|
||||||
application {
|
application {
|
||||||
module()
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
}
|
}
|
||||||
val response = client.get("/")
|
configureRouting()
|
||||||
|
}
|
||||||
|
val response = client.get("/health")
|
||||||
assertEquals(HttpStatusCode.OK, response.status)
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
assertEquals("Ktor: ${Greeting().greet()}", response.bodyAsText())
|
val body = response.bodyAsText()
|
||||||
|
assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body")
|
||||||
|
assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ rootProject.name = "recipe"
|
|||||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
|
includeBuild("build-logic")
|
||||||
repositories {
|
repositories {
|
||||||
google {
|
google {
|
||||||
mavenContent {
|
mavenContent {
|
||||||
|
|||||||
@@ -1,54 +1,45 @@
|
|||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
// AGP must apply BEFORE recipe.kotlin.multiplatform — the latter calls androidTarget(),
|
||||||
|
// which requires the Android Gradle Plugin to already be on the project. Gradle applies
|
||||||
|
// plugin IDs in declaration order, so com.android.library is listed first.
|
||||||
alias(libs.plugins.androidLibrary)
|
alias(libs.plugins.androidLibrary)
|
||||||
|
id("recipe.kotlin.multiplatform")
|
||||||
|
id("recipe.quality")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
androidTarget {
|
explicitApi()
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
iosArm64()
|
// Override framework baseName: shared exposes "Shared.framework" to Swift, while
|
||||||
iosSimulatorArm64()
|
// composeApp's convention-plugin default is "ComposeApp.framework". (D-07 / PITFALL #10)
|
||||||
|
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
|
||||||
jvm {
|
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework>().configureEach {
|
||||||
compilerOptions {
|
baseName = "Shared"
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
js {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// put your Multiplatform dependencies here
|
// Phase 1: intentionally empty. Domain models + DTOs land Phase 2+.
|
||||||
}
|
// D-19 / INFRA-06: Do NOT add Ktor, Compose, or SQLDelight deps here — EVER.
|
||||||
commonTest.dependencies {
|
|
||||||
implementation(libs.kotlin.test)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.ulfrx.recipe.shared"
|
namespace = "dev.ulfrx.recipe.shared"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk =
|
||||||
|
libs.versions.android.compileSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk =
|
||||||
|
libs.versions.android.minSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
||||||
class AndroidPlatform : Platform {
|
public class AndroidPlatform : Platform {
|
||||||
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = AndroidPlatform()
|
public actual fun getPlatform(): Platform = AndroidPlatform()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
const val SERVER_PORT = 8080
|
public const val SERVER_PORT: Int = 8080
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class Greeting {
|
public class Greeting {
|
||||||
private val platform = getPlatform()
|
private val platform = getPlatform()
|
||||||
|
|
||||||
fun greet(): String {
|
public fun greet(): String = "Hello, ${platform.name}!"
|
||||||
return "Hello, ${platform.name}!"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
interface Platform {
|
public interface Platform {
|
||||||
val name: String
|
public val name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
expect fun getPlatform(): Platform
|
public expect fun getPlatform(): Platform
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class SharedCommonTest {
|
class SharedCommonTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun example() {
|
fun example() {
|
||||||
assertEquals(3, 1 + 2)
|
assertEquals(3, 1 + 2)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import platform.UIKit.UIDevice
|
import platform.UIKit.UIDevice
|
||||||
|
|
||||||
class IOSPlatform : Platform {
|
public class IOSPlatform : Platform {
|
||||||
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
|
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = IOSPlatform()
|
public actual fun getPlatform(): Platform = IOSPlatform()
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package dev.ulfrx.recipe
|
|
||||||
|
|
||||||
class JsPlatform : Platform {
|
|
||||||
override val name: String = "Web with Kotlin/JS"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = JsPlatform()
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class JVMPlatform : Platform {
|
public class JVMPlatform : Platform {
|
||||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
override val name: String = "Java ${System.getProperty("java.version")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = JVMPlatform()
|
public actual fun getPlatform(): Platform = JVMPlatform()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class WasmPlatform : Platform {
|
public class WasmPlatform : Platform {
|
||||||
override val name: String = "Web with Kotlin/Wasm"
|
override val name: String = "Web with Kotlin/Wasm"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = WasmPlatform()
|
public actual fun getPlatform(): Platform = WasmPlatform()
|
||||||
|
|||||||
6
tools/verify-ios-flags.sh
Executable file
6
tools/verify-ios-flags.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Enforces INFRA-03 / D-18: iOS K/N flags present in gradle.properties.
|
||||||
|
set -euo pipefail
|
||||||
|
grep -q '^kotlin\.native\.binary\.gc=cms$' gradle.properties || { echo "MISSING: kotlin.native.binary.gc=cms" >&2; exit 1; }
|
||||||
|
grep -q '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties || { echo "MISSING: kotlin.native.binary.objcDisposeOnMain=false" >&2; exit 1; }
|
||||||
|
echo "OK: iOS binary flags present."
|
||||||
21
tools/verify-no-version-literals.sh
Executable file
21
tools/verify-no-version-literals.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Enforces INFRA-01 SC#2 / D-09: no literal *library/plugin* version strings outside catalog.
|
||||||
|
# Scans every *.gradle.kts for numeric version literals (e.g. version = "1.2.3") that would
|
||||||
|
# represent a library or plugin pin leaking out of the catalog.
|
||||||
|
#
|
||||||
|
# Exclusions (all semantic, not loopholes):
|
||||||
|
# - build-logic/build.gradle.kts needs literal plugin-dependency coordinates (`asDependency()`)
|
||||||
|
# - Top-level project-version assignments (unindented `^version = "x.y.z"`) are Gradle project
|
||||||
|
# metadata (artifact name) — NOT a library version pin. D-09 guards dependency versions,
|
||||||
|
# not project identity.
|
||||||
|
set -euo pipefail
|
||||||
|
VIOLATIONS=$(grep -rn -E 'version[[:space:]]*=[[:space:]]*"[0-9]' --include='*.gradle.kts' . 2>/dev/null \
|
||||||
|
| grep -v 'build-logic/build.gradle.kts' \
|
||||||
|
| grep -vE ':[0-9]+:version[[:space:]]*=[[:space:]]*"[0-9]' \
|
||||||
|
|| true)
|
||||||
|
if [ -n "$VIOLATIONS" ]; then
|
||||||
|
echo "ERROR: version literals found outside catalog:" >&2
|
||||||
|
echo "$VIOLATIONS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: no version literals outside catalog."
|
||||||
15
tools/verify-shared-pure.sh
Executable file
15
tools/verify-shared-pure.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Enforces INFRA-06 / D-19: shared/commonMain must not import Ktor, Compose, SQLDelight.
|
||||||
|
# Runs grep against shared/src/commonMain/ only. Allowed imports: kotlin.*, kotlinx.serialization, kotlinx.datetime.
|
||||||
|
set -euo pipefail
|
||||||
|
if [ ! -d shared/src/commonMain ]; then
|
||||||
|
echo "OK: shared/src/commonMain does not exist yet (pre-scaffold)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
VIOLATIONS=$(grep -rn -E '^import[[:space:]]+(io\.ktor|androidx\.compose|org\.jetbrains\.compose|app\.cash\.sqldelight)' shared/src/commonMain/ 2>/dev/null || true)
|
||||||
|
if [ -n "$VIOLATIONS" ]; then
|
||||||
|
echo "ERROR: shared/commonMain has forbidden imports:" >&2
|
||||||
|
echo "$VIOLATIONS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: shared/commonMain is pure."
|
||||||
Reference in New Issue
Block a user