---
phase: 01-project-infrastructure-module-wiring
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - gradle/libs.versions.toml
  - gradle.properties
  - tools/verify-no-version-literals.sh
  - tools/verify-shared-pure.sh
  - tools/verify-ios-flags.sh
autonomous: true
requirements: [INFRA-01, INFRA-03]
requirements_addressed: [INFRA-01, INFRA-03]

must_haves:
  truths:
    - "gradle/libs.versions.toml is the sole source of library/plugin versions (D-09 / INFRA-01 SC#2)"
    - "iOS K/N binary flags kotlin.native.binary.gc=cms and kotlin.native.binary.objcDisposeOnMain=false are set in gradle.properties (D-18 / INFRA-03)"
    - "Shell-based invariant checks (no-version-literals, shared-pure, ios-flags) are executable and fail-loud"
  artifacts:
    - path: "gradle/libs.versions.toml"
      provides: "Version + library + plugin aliases for Koin, Kermit, Spotless, Flyway, PostgreSQL JDBC, Ktor content-negotiation, Ktor JSON serializer"
      contains: "koin = ", "kermit = ", "spotless = ", "flyway = ", "postgresql ="
    - path: "gradle.properties"
      provides: "iOS K/N binary flags"
      contains: "kotlin.native.binary.gc=cms", "kotlin.native.binary.objcDisposeOnMain=false"
    - path: "tools/verify-no-version-literals.sh"
      provides: "Invariant check — no numeric version literals outside catalog in any *.gradle.kts (except build-logic/build.gradle.kts bootstrap coordinates)"
    - path: "tools/verify-shared-pure.sh"
      provides: "Invariant check — shared/src/commonMain must not import Ktor / Compose / SQLDelight"
    - path: "tools/verify-ios-flags.sh"
      provides: "Invariant check — both iOS K/N flags present in gradle.properties"
  key_links:
    - from: "build-logic/ (Plan 02)"
      to: "gradle/libs.versions.toml"
      via: "VersionCatalogsExtension.named(\"libs\").findLibrary(...) inside precompiled plugins"
      pattern: "findLibrary\\(\"koin-core\"\\)"
    - from: "gradle.properties"
      to: ":composeApp:linkDebugFrameworkIosSimulatorArm64"
      via: "Kotlin/Native compiler reads project properties at link time"
      pattern: "kotlin\\.native\\.binary\\."
---

<objective>
Extend the Gradle version catalog with every new alias required by Phase 1 (Koin, Kermit, Spotless, Flyway, Postgres JDBC, ktor-serverContentNegotiation, ktor-serializationKotlinxJson), append the two mandatory iOS Kotlin/Native binary flags to `gradle.properties`, and ship three shell-based invariant scripts under `tools/` that Plan 07 will use as phase-gate checks.

Purpose: This plan creates the **foundation** on which every other Phase 1 plan rests. Without these catalog entries, `build-logic/` (Plan 02) cannot resolve `findLibrary("koin-core")`; without the iOS flags, INFRA-03 fails silently. The verification scripts are required by 01-VALIDATION.md Wave 0 — every subsequent plan's `<automated>` block calls one of them.

Output: An extended `gradle/libs.versions.toml` (additive only, no version bumps to existing entries), extended `gradle.properties` with exactly two new lines, and three executable `.sh` scripts under a new `tools/` directory.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md
@.planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md
@.planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md
@.planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md
@gradle/libs.versions.toml
@gradle.properties
@CLAUDE.md

<interfaces>
<!-- Existing catalog aliases (do NOT rename or remove — only add new entries alongside) -->

From gradle/libs.versions.toml (current state, to extend):
```toml
[versions]
kotlin = "2.3.20"
ktor = "3.4.1"
composeMultiplatform = "1.10.3"
# (plus agp, androidx-*, composeHotReload, junit, kotlinx-coroutines, logback, material3)

[libraries]
# Existing: kotlin-test, kotlin-testJunit, junit, androidx-*, compose-*, kotlinx-coroutinesSwing,
#           logback, ktor-serverCore, ktor-serverNetty, ktor-serverTestHost

[plugins]
# Existing: androidApplication, androidLibrary, composeHotReload, composeMultiplatform,
#           composeCompiler, kotlinJvm, ktor, kotlinMultiplatform
```

From gradle.properties (current state — 10 lines of Kotlin + Gradle + Android config):
```properties
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
android.nonTransitiveRClass=true
android.useAndroidX=true
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Extend gradle/libs.versions.toml with Phase 1 aliases</name>
  <files>gradle/libs.versions.toml</files>
  <read_first>
    - gradle/libs.versions.toml (see current state of versions/libraries/plugins tables)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 110-175 (§ Standard Stack + Installation TOML fragments)
    - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 446-490 (delta blocks for [versions] / [libraries] / [plugins])
    - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-09 (catalog-only hard rule), D-14 (Koin deps needed), D-15 (Kermit), D-10 (Spotless), D-16 (Flyway + Postgres + content-negotiation)
  </read_first>
  <action>
  Extend `gradle/libs.versions.toml` with the new aliases for Phase 1. Preserve every existing entry verbatim (do NOT rename, remove, or bump any existing version).

  Append the following to `[versions]`, in the existing alphabetical-ish order:

  ```toml
  flyway = "12.4.0"
  kermit = "2.1.0"
  koin = "4.2.1"
  kotlinx-serialization = "1.7.3"
  postgresql = "42.7.10"
  spotless = "8.4.0"
  ```

  Append the following to `[libraries]`:

  ```toml
  # 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" }
  ```

  Append the following to `[plugins]`:

  ```toml
  spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
  flywayPlugin = { id = "org.flywaydb.flyway", version.ref = "flyway" }
  ```

  IMPORTANT invariants:
  - `koin-core`, `koin-compose`, `koin-compose-viewmodel`, `koin-android` have NO `version.ref` — they are BOM-managed by `koin-bom`.
  - `kotlin-test` is already in the catalog (line 22) — do NOT re-add.
  - Do NOT bump any existing version alias (kotlin, ktor, composeMultiplatform, logback, etc.).
  - The `koin-composeViewmodel` alias name uses camelCase (Gradle converts dashes-to-dots for accessors, but camelCase preserves `koin.composeViewmodel.get()`).
  </action>
  <verify>
    <automated>grep -E '^(flyway|kermit|koin|kotlinx-serialization|postgresql|spotless)\s*=' gradle/libs.versions.toml | wc -l | grep -q '^6$' && grep -E '^koin-bom\s*=' gradle/libs.versions.toml && grep -E '^koin-core\s*=' gradle/libs.versions.toml && grep -E '^koin-compose\s*=' gradle/libs.versions.toml && grep -E '^koin-composeViewmodel\s*=' gradle/libs.versions.toml && grep -E '^koin-android\s*=' gradle/libs.versions.toml && grep -E '^kermit\s*=' gradle/libs.versions.toml && grep -E '^ktor-serverContentNegotiation\s*=' gradle/libs.versions.toml && grep -E '^ktor-serializationKotlinxJson\s*=' gradle/libs.versions.toml && grep -E '^flyway-core\s*=' gradle/libs.versions.toml && grep -E '^flyway-database-postgresql\s*=' gradle/libs.versions.toml && grep -E '^postgresql\s*=' gradle/libs.versions.toml && grep -E '^spotless\s*=\s*\{\s*id\s*=' gradle/libs.versions.toml && grep -E '^flywayPlugin\s*=\s*\{\s*id\s*=' gradle/libs.versions.toml</automated>
  </verify>
  <acceptance_criteria>
    - `grep -E '^kotlin\s*=\s*"2\.3\.20"' gradle/libs.versions.toml` returns exactly 1 line (existing, unmodified)
    - `grep -E '^ktor\s*=\s*"3\.4\.1"' gradle/libs.versions.toml` returns exactly 1 line (existing, unmodified)
    - `grep -E '^koin\s*=\s*"4\.2\.1"' gradle/libs.versions.toml` returns exactly 1 line (new)
    - `grep -E '^kermit\s*=\s*"2\.1\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
    - `grep -E '^spotless\s*=\s*"8\.4\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
    - `grep -E '^flyway\s*=\s*"12\.4\.0"' gradle/libs.versions.toml` returns exactly 1 line (new)
    - `grep -E '^postgresql\s*=\s*"42\.7\.10"' gradle/libs.versions.toml` returns exactly 1 line (new)
    - `grep -c '^koin-' gradle/libs.versions.toml` returns `5` (koin-bom, koin-core, koin-compose, koin-composeViewmodel, koin-android)
    - `grep -c '^flyway-' gradle/libs.versions.toml` returns `2` (flyway-core, flyway-database-postgresql)
    - `grep -E '^\s*module\s*=\s*"io.insert-koin:koin-core"' gradle/libs.versions.toml` returns 1 line with NO `version.ref` attribute on same line (BOM-managed)
  </acceptance_criteria>
  <done>All Phase 1 catalog aliases present; no existing aliases modified; file parses as valid TOML.</done>
</task>

<task type="auto">
  <name>Task 2: Append iOS K/N binary flags to gradle.properties</name>
  <files>gradle.properties</files>
  <read_first>
    - gradle.properties (see current 10-line content)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1082-1107 (§ `gradle.properties` — iOS binary flags — exact content to append)
    - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-18 (INFRA-03, PITFALL #1)
    - CLAUDE.md convention #7 (iOS binary flags on day 1)
  </read_first>
  <action>
  Append the following 5 lines to `gradle.properties` exactly as shown (including the blank separator line and both comment lines). Do NOT modify any existing line:

  ```properties

  # 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
  ```

  IMPORTANT:
  - Place AT THE END of the file (append). The existing `android.useAndroidX=true` stays as the last non-iOS line.
  - Use EXACTLY the property keys `kotlin.native.binary.gc` and `kotlin.native.binary.objcDisposeOnMain`. Do not add quotes, spaces, or alternate spellings (the K/N compiler reads these keys literally).
  - Value `cms` is lowercase. Value `false` is lowercase.
  </action>
  <verify>
    <automated>grep -E '^kotlin\.native\.binary\.gc=cms$' gradle.properties | wc -l | grep -q '^1$' && grep -E '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties | wc -l | grep -q '^1$'</automated>
  </verify>
  <acceptance_criteria>
    - `grep -cE '^kotlin\.native\.binary\.gc=cms$' gradle.properties` returns `1`
    - `grep -cE '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.properties` returns `1`
    - `grep -c '^kotlin\.code\.style=official$' gradle.properties` returns `1` (unmodified existing)
    - `grep -c '^android\.useAndroidX=true$' gradle.properties` returns `1` (unmodified existing)
    - No duplicate of either flag (run grep twice — expect `1` each time, not `2`)
  </acceptance_criteria>
  <done>Both iOS K/N flags present once; original 10 lines unchanged.</done>
</task>

<task type="auto">
  <name>Task 3: Create verify-*.sh invariant scripts under tools/</name>
  <files>tools/verify-no-version-literals.sh, tools/verify-shared-pure.sh, tools/verify-ios-flags.sh</files>
  <read_first>
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1174-1218 (§ tools/verify-*.sh — canonical shell sketches)
    - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 1174-1218 (same scripts, same content — Pattern Map confirms no in-repo analog)
    - .planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md lines 62-79 (Wave 0 Requirements — these three scripts gate every task's `<automated>` check)
  </read_first>
  <action>
  Create the three executable bash scripts under `tools/` (create the directory — it does not exist yet). Each must be marked executable (`chmod +x`).

  **File 1: `tools/verify-no-version-literals.sh`** (enforces D-09 / INFRA-01 SC#2):

  ```sh
  #!/usr/bin/env bash
  # Enforces INFRA-01 SC#2 / D-09: no literal version strings outside catalog.
  # Scans every *.gradle.kts for numeric version literals (e.g. version = "1.2.3"),
  # excluding build-logic/build.gradle.kts which needs literal asDependency() coordinates.
  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' || 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."
  ```

  **File 2: `tools/verify-shared-pure.sh`** (enforces INFRA-06 / D-19):

  ```sh
  #!/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."
  ```

  **File 3: `tools/verify-ios-flags.sh`** (enforces INFRA-03 / D-18):

  ```sh
  #!/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."
  ```

  After writing all three files, run: `chmod +x tools/verify-no-version-literals.sh tools/verify-shared-pure.sh tools/verify-ios-flags.sh`.

  IMPORTANT:
  - Use `#!/usr/bin/env bash` (not `#!/bin/sh`) — `set -euo pipefail` requires bash semantics.
  - `tools/verify-shared-pure.sh` deliberately returns 0 if `shared/src/commonMain` does not exist (pre-scaffold state). This lets Plan 07 run the script before Plan 07 itself creates the scaffold.
  - `tools/verify-no-version-literals.sh` excludes `build-logic/build.gradle.kts` (its `asDependency()` trick requires literal plugin version coordinates — D-09 acknowledged exception).
  </action>
  <verify>
    <automated>test -x tools/verify-no-version-literals.sh && test -x tools/verify-shared-pure.sh && test -x tools/verify-ios-flags.sh && bash tools/verify-ios-flags.sh && bash tools/verify-shared-pure.sh && bash tools/verify-no-version-literals.sh</automated>
  </verify>
  <acceptance_criteria>
    - `test -f tools/verify-no-version-literals.sh && test -x tools/verify-no-version-literals.sh` succeeds
    - `test -f tools/verify-shared-pure.sh && test -x tools/verify-shared-pure.sh` succeeds
    - `test -f tools/verify-ios-flags.sh && test -x tools/verify-ios-flags.sh` succeeds
    - `bash tools/verify-ios-flags.sh` exits 0 and prints `OK: iOS binary flags present.` (proves Task 2 wrote flags)
    - `bash tools/verify-shared-pure.sh` exits 0 (current `shared/src/commonMain/kotlin/dev/ulfrx/recipe/` has only Greeting.kt/Platform.kt/Constants.kt — no ktor/compose imports)
    - `bash tools/verify-no-version-literals.sh` exits 0 (current *.gradle.kts files use `libs.plugins.*` aliases — no literal versions)
    - Each script has `#!/usr/bin/env bash` as line 1
    - Each script uses `set -euo pipefail`
  </acceptance_criteria>
  <done>Three executable verification scripts exist, each runs green against the current repo state.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| developer → Gradle build | Local-only; Gradle reads `libs.versions.toml` + `gradle.properties` verbatim. No untrusted input. |
| Gradle → Maven Central + Gradle Plugin Portal | Existing repository declarations in `settings.gradle.kts` (Plan 03 doesn't change them). Pinned versions via catalog reduce supply-chain drift. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-01-01 | Tampering (supply chain) | `gradle/libs.versions.toml` new entries | mitigate | All new version refs are pinned to specific stable releases (`koin = "4.2.1"`, `kermit = "2.1.0"`, `flyway = "12.4.0"`, `spotless = "8.4.0"`, `postgresql = "42.7.10"`) — no version ranges, no `latest.release`. Gradle verifies SHA-256 via `gradle/verification-metadata.xml` if enabled in later phases. |
| T-01-01-02 | Tampering | `tools/*.sh` scripts | accept | Scripts live in repo and run locally; their only effect is exit 0/1. Read `gradle.properties` and `*.gradle.kts` only — no network I/O, no write. Risk = low. |
| T-01-01-03 | Information Disclosure | `gradle.properties` iOS flags | accept | Flag values (`cms`, `false`) are build configuration, not secrets. Public in every iOS KMP tutorial. |
| T-01-01-04 | Denial of Service | wrong catalog syntax breaks build | mitigate | Task 1 `<acceptance_criteria>` greps for exact alias presence; Wave 2 plans that consume the catalog will fail fast if an alias is misspelled. |
</threat_model>

<verification>
Phase-level verification for this plan:
- All three `tools/verify-*.sh` scripts run green against the post-plan repo.
- `gradle/libs.versions.toml` parses (Gradle will surface a TOML parse error at next `./gradlew` invocation in Plan 02).
- `gradle.properties` has exactly two new iOS K/N flag lines and is otherwise byte-identical to its pre-plan content.

No Gradle build is expected to run fully in this plan — we have not yet scaffolded `build-logic/` (Plan 02) nor refactored modules (Plan 03), so `./gradlew build` would fail to resolve the new library aliases. Catalog additions ARE safe for Gradle configuration though (unused entries are inert).
</verification>

<success_criteria>
- `tools/verify-ios-flags.sh` exits 0
- `tools/verify-no-version-literals.sh` exits 0
- `tools/verify-shared-pure.sh` exits 0
- Catalog contains 6 new `[versions]` keys (flyway, kermit, koin, kotlinx-serialization, postgresql, spotless)
- Catalog contains 10 new `[libraries]` entries (5 koin-*, kermit, 2 ktor-*, 2 flyway-*, postgresql)
- Catalog contains 2 new `[plugins]` entries (spotless, flywayPlugin)
</success_criteria>

<output>
After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-01-SUMMARY.md` recording: catalog entries added (count), gradle.properties append location, shell-script paths, and any deviation from the planned version pins (if Maven Central shows a newer stable, record the downgrade decision).
</output>
