---
phase: 1
slug: project-infrastructure-module-wiring
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-24
---

# Phase 1 — Validation Strategy

> Per-phase validation contract derived from `01-RESEARCH.md § Validation Architecture`. Phase 1 is predominantly **build-level** verification (Gradle tasks, file structure, grep invariants) rather than unit tests. The existing `ApplicationTest.kt` is the one test file extended (adds `/health` coverage).

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | `kotlin.test` (commonTest) + `ktor-server-test-host` (JUnit 4 runner for server) + existing KMP template test stubs |
| **Config file** | `composeApp/src/commonTest/kotlin/ComposeAppCommonTest.kt`, `shared/src/commonTest/kotlin/SharedCommonTest.kt`, `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` (all present from template) |
| **Quick run command** | `./gradlew :server:test :composeApp:jvmTest :shared:jvmTest` (JVM-only, <30s) |
| **Full suite command** | `./gradlew check` (runs `spotlessCheck` + every `*Test` task across all targets) |
| **Estimated runtime** | ~30s quick / ~3–5 min full (cold) |

---

## Sampling Rate

- **After every task commit:** `./gradlew spotlessCheck :server:test :shared:jvmTest` (fast subset, <30s)
- **After every plan wave:** `./gradlew build` (includes iOS framework link + Android APK)
- **Before `/gsd-verify-work` (phase gate):** `./gradlew check` + manual server `/health` curl + iOS simulator boot check
- **Max feedback latency:** 30s (quick subset) / 5 min (full)

---

## Per-Task Verification Map

**Note:** Task IDs are populated by `gsd-planner` when PLAN.md files are written. Each row below is the per-requirement contract the planner MUST map to at least one task's `<automated>` block. Rows marked "Wave 0" require a helper file to be created before task execution can verify it.

| Behavior | Requirement | Test Type | Automated Command | File Exists | Status |
|----------|-------------|-----------|-------------------|-------------|--------|
| No version literals in any `build.gradle.kts` | INFRA-01 | shell grep | `tools/verify-no-version-literals.sh` | ❌ Wave 0 | ⬜ pending |
| `gradle/libs.versions.toml` is the single source of truth | INFRA-01 | grep | `grep -rE "libs\\.(versions\|plugins\|bundles)" build-logic/src/main/kotlin/` returns all version lookups | ✅ catalog exists | ⬜ pending |
| Convention plugins apply without duplication | INFRA-02 | Gradle | `./gradlew :composeApp:help :server:help :shared:help` shows `recipe.*` in applied plugins | ❌ Wave 0 (plugins don't exist yet) | ⬜ pending |
| Adding a new KMP module only needs `id("recipe.kotlin.multiplatform")` | INFRA-02 | visual | refactored `shared/build.gradle.kts` ≤15 LOC | Target Wave 2 | ⬜ pending |
| `gradle.properties` contains both iOS K/N flags | INFRA-03 | grep | `tools/verify-ios-flags.sh` | ❌ Wave 0 | ⬜ pending |
| iOS simulator build has no legacy memory-manager warnings | INFRA-03 | build-log | `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 --info 2>&1 \| grep -iE 'legacy\|freeze\|SharedImmutable'` is empty | Wave 2 (iOS) | ⬜ pending |
| `shared/commonMain` has no Ktor/Compose/SQLDelight imports | INFRA-06 | grep | `tools/verify-shared-pure.sh` | ❌ Wave 0 | ⬜ pending |
| `shared/` package scaffold exists | INFRA-06 | file | `test -d shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared` | Wave 2 | ⬜ pending |
| SC1: `./gradlew build` succeeds + produces iOS framework + APK | ROADMAP SC1 | Gradle | `./gradlew build && test -f composeApp/build/outputs/apk/debug/composeApp-debug.apk && test -d composeApp/build/bin/iosSimulatorArm64/debugFramework/ComposeApp.framework` | Phase gate | ⬜ pending |
| SC4: each module's `help` shows its convention plugins | ROADMAP SC4 | Gradle | `./gradlew :composeApp:help -q \| grep 'recipe.kotlin.multiplatform'` etc. | Phase gate | ⬜ pending |
| Server `/health` returns 200 JSON `{"status":"ok"}` | D-16 | integration | `./gradlew :server:test --tests "*HealthRoute*"` (added to ApplicationTest.kt) | ❌ Wave 0 (test update) | ⬜ pending |
| Server fails loudly if Postgres unreachable | D-16 | manual | `docker compose down; ./gradlew :server:run` exits non-zero with "Database unreachable" in logs | Phase gate | ⬜ pending |
| Spotless formatting clean | D-10 | Gradle | `./gradlew spotlessCheck` | Per-commit | ⬜ pending |
| Koin starts without double-init | D-14 | Gradle test | `./gradlew :composeApp:jvmTest` (template test exercises App() composition path; no `KoinApplicationAlreadyStartedException`) | Per-wave | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

These assets MUST exist before any verification task can run green. The planner should place them in Wave 0 (or inside the plan that creates the infrastructure they verify).

- [ ] `tools/verify-no-version-literals.sh` — greps every `build.gradle.kts` + `build-logic/**/*.gradle.kts` for a non-test numeric version literal; exits non-zero on match
- [ ] `tools/verify-shared-pure.sh` — greps `shared/src/commonMain/` for forbidden imports (`io.ktor`, `androidx.compose`, `org.jetbrains.compose`, `app.cash.sqldelight`); exits non-zero on match
- [ ] `tools/verify-ios-flags.sh` — greps `gradle.properties` for `kotlin.native.binary.objcDisposeOnMain=false` AND `kotlin.native.binary.gc=cms`; exits non-zero if either is missing
- [ ] `build-logic/` scaffold — `settings.gradle.kts`, `build.gradle.kts`, and 5 `src/main/kotlin/recipe.*.gradle.kts` stubs
- [ ] `server/src/main/resources/application.conf` — HOCON with `ktor.deployment`, `database.url/user/password` using `${?X}` env overrides
- [ ] `server/src/main/resources/db/migration/.gitkeep` — directory placeholder for Flyway
- [ ] `docker-compose.yml` — `postgres:16` service with named volume + healthcheck
- [ ] `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` — extended with `/health` endpoint assertion
- [ ] `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt` + `AppModule.kt` — `initKoin()` helper + empty module
- [ ] `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt` — Kermit `setTag("recipe")`
- [ ] `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt` + `AndroidManifest.xml` registration — calls `initKoin { androidContext(this) }`
- [ ] `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt` — `fun doInitKoin()` exported for Swift
- [ ] `iosApp/iosApp/iOSApp.swift` — modified to call `KoinIosKt.doInitKoin()` in `init()`

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| iOS simulator debug launch has no legacy K/N memory-manager warnings | INFRA-03 / SC3 | Requires Xcode simulator boot; not scriptable from Gradle reliably on CI | Run `./gradlew :composeApp:iosSimulatorArm64Test` OR open `iosApp.xcworkspace` in Xcode, run on iPhone 15 simulator, inspect console for `legacy`/`freeze`/`SharedImmutable` — expect none |
| Hot-reload dev loop on Desktop still works post-refactor (regression check for commit c50d747) | — | Interactive | `./gradlew :composeApp:jvmRun --mainClass MainKt --auto-reload`; edit `App.kt`, observe reload without rebuild |
| Server `/health` reachable via curl when Postgres up | D-16 | Requires running Postgres + server process | `docker compose up -d postgres`, `./gradlew :server:run &`, `sleep 5`, `curl -sf http://localhost:8080/health` returns `{"status":"ok"}` |

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references (13 items listed above)
- [ ] No watch-mode flags in any verification command
- [ ] Feedback latency < 30s (quick) / 5min (full)
- [ ] `nyquist_compliant: true` set in frontmatter after planner maps every task to a row above

**Approval:** pending
