20 KiB
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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.mdFrom gradle/libs.versions.toml (current state, to extend):
[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):
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
Append the following to [versions], in the existing alphabetical-ish order:
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]:
# 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]:
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-androidhave NOversion.ref— they are BOM-managed bykoin-bom.kotlin-testis already in the catalog (line 22) — do NOT re-add.- Do NOT bump any existing version alias (kotlin, ktor, composeMultiplatform, logback, etc.).
- The
koin-composeViewmodelalias name uses camelCase (Gradle converts dashes-to-dots for accessors, but camelCase preserveskoin.composeViewmodel.get()). 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*{\sid\s=' gradle/libs.versions.toml && grep -E '^flywayPlugin\s*=\s*{\sid\s=' gradle/libs.versions.toml <acceptance_criteria>grep -E '^kotlin\s*=\s*"2\.3\.20"' gradle/libs.versions.tomlreturns exactly 1 line (existing, unmodified)grep -E '^ktor\s*=\s*"3\.4\.1"' gradle/libs.versions.tomlreturns exactly 1 line (existing, unmodified)grep -E '^koin\s*=\s*"4\.2\.1"' gradle/libs.versions.tomlreturns exactly 1 line (new)grep -E '^kermit\s*=\s*"2\.1\.0"' gradle/libs.versions.tomlreturns exactly 1 line (new)grep -E '^spotless\s*=\s*"8\.4\.0"' gradle/libs.versions.tomlreturns exactly 1 line (new)grep -E '^flyway\s*=\s*"12\.4\.0"' gradle/libs.versions.tomlreturns exactly 1 line (new)grep -E '^postgresql\s*=\s*"42\.7\.10"' gradle/libs.versions.tomlreturns exactly 1 line (new)grep -c '^koin-' gradle/libs.versions.tomlreturns5(koin-bom, koin-core, koin-compose, koin-composeViewmodel, koin-android)grep -c '^flyway-' gradle/libs.versions.tomlreturns2(flyway-core, flyway-database-postgresql)grep -E '^\s*module\s*=\s*"io.insert-koin:koin-core"' gradle/libs.versions.tomlreturns 1 line with NOversion.refattribute on same line (BOM-managed) </acceptance_criteria> All Phase 1 catalog aliases present; no existing aliases modified; file parses as valid TOML.
# 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=truestays as the last non-iOS line. - Use EXACTLY the property keys
kotlin.native.binary.gcandkotlin.native.binary.objcDisposeOnMain. Do not add quotes, spaces, or alternate spellings (the K/N compiler reads these keys literally). - Value
cmsis lowercase. Valuefalseis lowercase. 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$' <acceptance_criteria>grep -cE '^kotlin\.native\.binary\.gc=cms$' gradle.propertiesreturns1grep -cE '^kotlin\.native\.binary\.objcDisposeOnMain=false$' gradle.propertiesreturns1grep -c '^kotlin\.code\.style=official$' gradle.propertiesreturns1(unmodified existing)grep -c '^android\.useAndroidX=true$' gradle.propertiesreturns1(unmodified existing)- No duplicate of either flag (run grep twice — expect
1each time, not2) </acceptance_criteria> Both iOS K/N flags present once; original 10 lines unchanged.
File 1: tools/verify-no-version-literals.sh (enforces D-09 / INFRA-01 SC#2):
#!/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):
#!/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):
#!/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 pipefailrequires bash semantics. tools/verify-shared-pure.shdeliberately returns 0 ifshared/src/commonMaindoes not exist (pre-scaffold state). This lets Plan 07 run the script before Plan 07 itself creates the scaffold.tools/verify-no-version-literals.shexcludesbuild-logic/build.gradle.kts(itsasDependency()trick requires literal plugin version coordinates — D-09 acknowledged exception). 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 <acceptance_criteria>test -f tools/verify-no-version-literals.sh && test -x tools/verify-no-version-literals.shsucceedstest -f tools/verify-shared-pure.sh && test -x tools/verify-shared-pure.shsucceedstest -f tools/verify-ios-flags.sh && test -x tools/verify-ios-flags.shsucceedsbash tools/verify-ios-flags.shexits 0 and printsOK: iOS binary flags present.(proves Task 2 wrote flags)bash tools/verify-shared-pure.shexits 0 (currentshared/src/commonMain/kotlin/dev/ulfrx/recipe/has only Greeting.kt/Platform.kt/Constants.kt — no ktor/compose imports)bash tools/verify-no-version-literals.shexits 0 (current *.gradle.kts files uselibs.plugins.*aliases — no literal versions)- Each script has
#!/usr/bin/env bashas line 1 - Each script uses
set -euo pipefail</acceptance_criteria> Three executable verification scripts exist, each runs green against the current repo state.
<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> |
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).
<success_criteria>
tools/verify-ios-flags.shexits 0tools/verify-no-version-literals.shexits 0tools/verify-shared-pure.shexits 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>