initial skills

This commit is contained in:
nrobi144
2025-12-30 15:18:48 +02:00
parent 258c4e0111
commit f654af9d8a
23 changed files with 10898 additions and 0 deletions
@@ -0,0 +1,400 @@
---
name: kotlin-multiplatform
description: |
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific,
source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets
(Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues.
Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation,
build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
---
# Kotlin Multiplatform: Platform Abstraction Decisions
Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific.
## When to Use This Skill
Making platform abstraction decisions:
- "Should I create expect/actual or keep Android-only?"
- "Can I share this ViewModel logic?"
- "Where does this crypto/JSON/network implementation belong?"
- "This uses Android Context - can it be abstracted?"
- "Is this code in the wrong module?"
- Preparing for iOS/web/wasm targets
- Detecting incorrect placements
## Abstraction Decision Tree
**Central question:** "Should this code be reused across platforms?"
Follow this decision path (< 1 minute):
```
Q: Is it used by 2+ platforms?
├─ NO → Keep platform-specific
│ Example: Android-only permission handling
└─ YES → Continue ↓
Q: Is it pure Kotlin (no platform APIs)?
├─ YES → commonMain
│ Example: Nostr event parsing, business rules
└─ NO → Continue ↓
Q: Does it vary by platform or by JVM vs non-JVM?
├─ By platform (Android ≠ iOS ≠ Desktop)
│ → expect/actual
│ Example: Secp256k1Instance (uses different security APIs)
├─ By JVM (Android = Desktop ≠ iOS/web)
│ → jvmAndroid
│ Example: Jackson JSON parsing (JVM library)
└─ Complex/UI-related
→ Keep platform-specific
Example: Navigation (Activity vs Window too different)
Final check:
Q: Maintenance cost of abstraction < duplication cost?
├─ YES → Proceed with abstraction
└─ NO → Duplicate (simpler)
```
### Real Examples from Codebase
**Crypto → expect/actual:**
```kotlin
// commonMain - expect declaration
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}
// androidMain - uses Android Keystore
// jvmMain - uses Desktop JVM crypto
// iosMain - uses iOS Security framework
```
**Why:** Each platform has different security APIs.
**JSON parsing → jvmAndroid:**
```kotlin
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}
```
**Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web.
**Navigation → platform-specific:**
- Android: `MainActivity` (Activity + Compose Navigation)
- Desktop: `Window` + sidebar + MenuBar
**Why:** UI paradigms fundamentally different.
## Mental Model: Source Sets as Dependency Graph
Think of source sets as a dependency graph, not folders.
```
┌─────────────────────────────────────────────┐
│ commonMain = Contract (pure Kotlin) │
│ - Business logic, protocol, data models │
│ - No platform APIs │
└────────────┬────────────────────────────────┘
├──────────────────────┬────────────────────
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ jvmAndroid │ │ iosMain │
│ JVM libs shared │ │ iOS common │
│ - Jackson │ │ │
│ - OkHttp │ └────┬─────────────┘
└───┬───────────┬───┘ │
│ │ ├─→ iosX64Main
▼ ▼ ├─→ iosArm64Main
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
│android │ │jvmMain │
│Main │ │(Desktop) │
└─────────┘ └──────────┘
Future: jsMain, wasmMain
```
**Key insight:** jvmAndroid is NOT a platform - it's a shared JVM layer.
## The jvmAndroid Pattern
**Unique to Amethyst.** Shares JVM libraries between Android + Desktop.
### When to Use jvmAndroid
Use jvmAndroid when:
- ✅ JVM-specific libraries (Jackson, OkHttp, url-detector)
- ✅ Android implementation = Desktop implementation (same JVM)
- ✅ Library doesn't work on iOS/web
Do NOT use jvmAndroid for:
- ❌ Pure Kotlin code (use commonMain)
- ❌ Platform-specific APIs (use androidMain/jvmMain)
- ❌ Code that should work on all platforms
### Example from quartz/build.gradle.kts
```kotlin
// Must be defined BEFORE androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
api(libs.jackson.module.kotlin) // JSON parsing - JVM only
api(libs.url.detector) // URL extraction - JVM only
implementation(libs.okhttp) // HTTP client - JVM only
}
}
// Both depend on jvmAndroid
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
```
**Why Jackson in jvmAndroid, not commonMain?**
- Jackson is JVM-specific library
- Works on Android (runs on JVM)
- Works on Desktop (runs on JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
**Web/wasm consideration:** For future web support, consider migrating from Jackson → kotlinx.serialization (see Target-Specific Guidance).
## What to Abstract vs Keep Platform-Specific
Quick decision guidelines based on codebase patterns:
### Always Abstract
- **Crypto** (Secp256k1, encryption, signing)
- **Core protocol logic** (Nostr events, NIPs)
- **Why:** Needed everywhere, platform security APIs vary
### Often Abstract
- **I/O operations** (file reading, caching)
- **Logging** (platform logging systems differ)
- **Serialization** (if using kotlinx.serialization)
- **Why:** Commonly reused, platform implementations available
### Sometimes Abstract
- **Business logic:** YES - state machines, data processing
- **UI state:** NO - ViewModels platform-specific
- **Why:** Separate concerns, business logic reusable
### Rarely Abstract
- **UI components** (composables with platform dependencies)
- **Why:** Platform paradigms differ (bottom nav vs sidebar)
### Never Abstract
- **Navigation** (Activity vs Window fundamentally different)
- **Permissions** (Android vs iOS APIs incompatible)
- **Platform UX patterns**
- **Why:** Too platform-specific, abstraction creates leaky APIs
### Evidence from shared-ui-analysis.md
| Component | Shared? | Rationale |
|-----------|---------|-----------|
| PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs |
| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings |
| Navigation (INav) | ❌ NO | Activity vs Window too different |
| AccountViewModel | ⚠️ PARTIAL | Business logic → IAccountState (shared), UI state → platform ViewModels |
| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper |
## expect/actual Mechanics
**When to use:** Code needed by 2+ platforms, varies by platform.
### Pattern Categories from Codebase
**Objects (singletons):**
```kotlin
// 24 expect declarations found, common pattern:
expect object Secp256k1Instance { ... }
expect object Log { ... }
expect object LibSodiumInstance { ... }
```
**Classes (instantiable):**
```kotlin
expect class AESCBC { ... }
expect class DigestInstance { ... }
```
**Functions (utilities):**
```kotlin
expect fun platform(): String
expect fun currentTimeSeconds(): Long
```
**See** [references/expect-actual-catalog.md](references/expect-actual-catalog.md) for complete catalog with rationale.
## Target-Specific Guidance
### Android, JVM (Desktop), iOS - Current Primary Targets
**Status:** Mature patterns, stable APIs
**Android (androidMain):**
- Uses Android framework (Activity, Context, etc.)
- secp256k1-kmp-jni-android for crypto
- AndroidX libraries
**Desktop JVM (jvmMain):**
- Uses Compose Desktop (Window, MenuBar, etc.)
- secp256k1-kmp-jni-jvm for crypto
- Pure JVM libraries
**iOS (iosMain):**
- Active development, framework configured
- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main
- Platform APIs via platform.posix, Security framework
### Web, wasm - Future Targets
**Status:** Not yet implemented, consider for future-proofing
**Constraints to know:**
- ❌ No platform.posix (file I/O different)
- ❌ No JVM libraries (Jackson, OkHttp won't work)
- ❌ Different async model (JS event loop vs threads)
**Future-proofing tips:**
1. Prefer pure Kotlin in commonMain
2. Use kotlinx.* libraries:
- kotlinx.serialization instead of Jackson
- ktor instead of OkHttp (ktor supports web)
- kotlinx.datetime instead of custom date handling
3. Avoid platform.posix for file operations
4. Test abstractions work without JVM assumptions
**Example migration path:**
```kotlin
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
```
## Integration: When to Invoke Other Skills
### Invoke gradle-expert
Trigger gradle-expert skill when encountering:
- Dependency conflicts (e.g., secp256k1-android vs secp256k1-jvm version mismatch)
- Build errors related to source sets
- Version catalog issues (libs.versions.toml)
- "Duplicate class" errors
- Performance/build time issues
**Example trigger:**
```
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
```
→ Invoke gradle-expert for dependency conflict resolution.
### Flags to Raise
**Platform code in commonMain:**
```kotlin
// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context // Context is Android-only!
```
→ Flag: "Android API in commonMain won't compile on other platforms"
**Duplicated business logic:**
```kotlin
// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // Duplicated!
```
→ Flag: "Business logic duplicated, should be in commonMain or expect/actual"
**Reinventing wheel - suggest KMP alternatives:**
- Custom date/time → kotlinx.datetime
- OkHttp → ktor (supports web)
- Jackson → kotlinx.serialization
- Custom UUID → kotlinx.uuid (when stable)
## Common Pitfalls
### 1. Over-Abstraction
**Problem:** Creating expect/actual for UI components
```kotlin
// ❌ BAD
expect fun NavigationComponent(...)
```
**Why:** Navigation paradigms too different (Activity vs Window)
**Fix:** Keep platform-specific, accept duplication
### 2. Under-Sharing
**Problem:** Duplicating business logic across platforms
```kotlin
// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }
```
**Why:** Bug fixes need to be applied twice, tests duplicated
**Fix:** Move to commonMain (pure Kotlin) or create expect/actual
### 3. Leaky Abstractions
**Problem:** Platform code in commonMain
```kotlin
// commonMain - ❌ BAD
import android.content.Context // Won't compile on iOS!
```
**Fix:** Use expect/actual or dependency injection
### 4. Premature Abstraction
**Problem:** Creating expect/actual before second platform needs it
```kotlin
// ❌ BAD - only used on Android currently
expect fun showNotification(...)
```
**Why:** Wrong abstraction boundaries, wasted effort
**Fix:** Wait until iOS actually needs it, then abstract
### 5. Wrong Source Set
**Problem:** JVM libraries in commonMain
```kotlin
// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper
```
**Why:** Jackson won't compile on iOS/web
**Fix:** Move to jvmAndroid or migrate to kotlinx.serialization
## Quick Reference
| Code Type | Recommended Location | Reason |
|-----------|---------------------|--------|
| Pure Kotlin business logic | commonMain | Works everywhere |
| Nostr protocol, NIPs | commonMain | Core logic, no platform APIs |
| JVM libs (Jackson, OkHttp) | jvmAndroid | Android + Desktop only |
| Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform |
| I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ |
| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns |
| State (UI) | Platform ViewModels | Platform-specific lifecycle |
| UI formatters (pure) | commons/commonMain | Reusable, no dependencies |
| UI components (complex) | Platform-specific | Paradigms differ |
| Navigation | Platform-specific only | Activity vs Window too different |
| Permissions | Platform-specific only | APIs incompatible |
| Platform UX (menus, etc.) | Platform-specific only | Native feel required |
## See Also
- [references/abstraction-examples.md](references/abstraction-examples.md) - Good/bad abstraction examples with rationale
- [references/source-set-hierarchy.md](references/source-set-hierarchy.md) - Visual hierarchy with Amethyst examples
- [references/expect-actual-catalog.md](references/expect-actual-catalog.md) - All 24 expect/actual pairs with "why abstracted"
- [references/target-compatibility.md](references/target-compatibility.md) - Platform constraints and future-proofing
## Scripts
- `scripts/validate-kmp-structure.sh` - Detect incorrect placements, validate source sets
- `scripts/suggest-kmp-dependency.sh` - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)
@@ -0,0 +1,311 @@
# Abstraction Examples from Amethyst Codebase
Real examples of abstraction decisions with rationale.
## Good Abstractions (Why They Work)
### 1. Secp256k1Instance - Crypto Signing
**Location:** expect in commonMain, actual in androidMain/jvmMain/iosMain
**Code:**
```kotlin
// quartz/src/commonMain/.../Secp256k1Instance.kt
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
fun verifySchnorr(signature: ByteArray, hash: ByteArray, pubKey: ByteArray): Boolean
}
```
**Why abstracted:**
- Used by all platforms (Android, Desktop, iOS)
- Security APIs fundamentally different:
- Android: secp256k1-kmp-jni-android (Android Keystore integration)
- Desktop: secp256k1-kmp-jni-jvm (pure JVM crypto)
- iOS: Native Security framework
- Core protocol requirement (Nostr signatures)
**Decision rationale:** Always abstract crypto - varies by platform security APIs, critical for all platforms.
---
### 2. Log - Platform Logging
**Location:** expect object in commonMain
**Code:**
```kotlin
// quartz/src/commonMain/.../Log.kt
expect object Log {
fun d(tag: String, message: String)
fun w(tag: String, message: String, throwable: Throwable?)
fun e(tag: String, message: String, throwable: Throwable?)
}
```
**Why abstracted:**
- Used throughout quartz module (protocol library)
- Logging systems differ:
- Android: android.util.Log
- Desktop: println or logging framework
- iOS: NSLog or OSLog
- Simple interface, easy to implement
**Decision rationale:** Often abstract logging - platform systems differ, widely used, simple interface.
---
### 3. Platform Utils - Time & Platform Name
**Location:** expect functions in commonMain
**Code:**
```kotlin
// quartz/src/commonMain/.../Platform.kt
expect fun platform(): String
expect fun currentTimeSeconds(): Long
```
**Why abstracted:**
- Used by Nostr event creation (timestamps)
- Platform name for debugging
- Simple utilities, clear platform boundary
**Decision rationale:** Platform utilities are good abstraction candidates - simple, useful everywhere.
---
### 4. Jackson JSON (jvmAndroid Pattern)
**Location:** jvmAndroid source set
**Code:**
```kotlin
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin) // JVM-only library
}
```
**Why jvmAndroid (not commonMain):**
- Jackson is JVM-specific library
- Works on Android (JVM) + Desktop (JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
- Performance-critical JSON parsing
**Decision rationale:** Use jvmAndroid for JVM libraries shared between Android and Desktop.
**Future consideration:** For web support, migrate to kotlinx.serialization (works on all platforms).
---
## Bad/Over-Abstractions (Why They Failed)
### 1. Navigation Abstraction (Avoided)
**What COULD have been done:**
```kotlin
// ❌ Over-abstraction - DON'T DO THIS
expect interface Navigator {
fun navigate(route: String)
fun popBackStack()
}
```
**Why NOT abstracted:**
- Navigation paradigms fundamentally different:
- Android: Activity + Compose Navigation + back stack
- Desktop: Window + screen state + no back stack concept
- Complex APIs don't map well
- Creates leaky abstraction
**Actual approach:** Keep platform-specific
- Android: `INav` interface + Compose Navigation
- Desktop: Simple screen enum + state
**Decision rationale:** Never abstract navigation - platforms too different, abstraction would be leaky.
---
### 2. String Resources (Abstraction Planned)
**Current state:** Platform-specific (over-duplication)
**Problem:**
```kotlin
// Android uses R.string.*
Text(stringResource(R.string.post_not_found))
// Desktop uses hardcoded strings
Text("Post not found")
```
**Why NOT yet abstracted:** Waiting for second platform to fully implement UI, then will create StringProvider interface.
**Planned abstraction:**
```kotlin
// commonMain
interface StringProvider {
fun get(key: String): String
}
// androidMain
class AndroidStringProvider(context: Context): StringProvider { ... }
// jvmMain
class DesktopStringProvider: StringProvider { ... }
```
**Lesson:** Don't abstract prematurely - wait until second platform needs it, then create proper abstraction.
---
## Platform-Specific Code (Why NOT Abstracted)
### 1. MainActivity (Android Activity)
**Location:** amethyst/src/main/.../MainActivity.kt
**Code:**
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
setContent {
AmethystTheme {
AccountScreen(accountStateViewModel)
}
}
}
}
```
**Why platform-specific:**
- AppCompatActivity is Android framework
- Activity lifecycle unique to Android
- enableEdgeToEdge() is Android-specific API
- No equivalent on Desktop (uses Window)
**Decision rationale:** Android Activity is platform-specific by nature.
---
### 2. Desktop Window & MenuBar
**Location:** desktopApp/src/jvmMain/.../Main.kt
**Code:**
```kotlin
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Amethyst"
) {
MenuBar {
Menu("File") {
Item("New Note", onClick = { ... }, shortcut = KeyShortcut(Key.N, ctrl = true))
Item("Quit", onClick = ::exitApplication)
}
}
NavigationRail { ... } // Sidebar navigation
}
}
```
**Why platform-specific:**
- Window, MenuBar, NavigationRail are Compose Desktop APIs
- Keyboard shortcuts (Ctrl+N) are desktop paradigm
- Sidebar navigation vs Android bottom nav
- No equivalent on Android
**Decision rationale:** Desktop UX patterns are platform-specific by nature.
---
### 3. AccountViewModel (Android ViewModel)
**Location:** amethyst/.../AccountStateViewModel.kt
**Partially abstracted:**
- Business logic → IAccountState interface (can be shared)
- UI state + lifecycle → AndroidX ViewModel (Android-only)
**Why not fully abstracted:**
- AndroidX ViewModel lifecycle tied to Android
- Desktop doesn't need ViewModel (simpler state management)
- SavedStateHandle is Android-specific
**Decision rationale:** Extract business logic to interface, keep UI state platform-specific.
---
## Migration Examples (Android → Shared)
### Example 1: PubKeyFormatter (Pure Kotlin)
**Before:**
```kotlin
// amethyst/ui/note/PubKeyFormatter.kt
fun String.toDisplayHexKey(): String {
return "${take(8)}:${takeLast(8)}"
}
```
**After:**
```kotlin
// commons/commonMain/formatters/PubKeyFormatter.kt
fun String.toDisplayHexKey(): String {
return "${take(8)}:${takeLast(8)}"
}
// Both apps use it
import com.vitorpamplona.amethyst.commons.formatters.toDisplayHexKey
```
**Why successful:**
- Pure Kotlin, no platform dependencies
- Widely reused
- Simple utility function
---
### Example 2: TimeAgoFormatter (Requires Abstraction)
**Problem:**
```kotlin
// Uses Android R.string.*
fun timeAgo(timestamp: Long): String {
return context.getString(R.string.x_minutes_ago, minutes)
}
```
**Solution:** Abstract string resources
```kotlin
// commonMain
fun timeAgo(timestamp: Long, stringProvider: StringProvider): String {
return stringProvider.get("x_minutes_ago", minutes)
}
// androidMain
stringProvider = AndroidStringProvider(context)
// jvmMain
stringProvider = DesktopStringProvider()
```
**Why successful:** Clear platform boundary (string resources), useful on both platforms.
---
## Decision Pattern Summary
| Pattern | Abstract? | Why |
|---------|-----------|-----|
| Pure Kotlin utilities | ✅ YES | No platform dependency, easy |
| Crypto APIs | ✅ YES (expect/actual) | Platform security APIs differ |
| JVM libraries | ⚠️ jvmAndroid | Works on Android+Desktop only |
| UI components (simple) | ✅ YES | Composables work cross-platform |
| UI components (complex) | ❌ NO | Platform dependencies |
| Navigation | ❌ NO | Paradigms too different |
| ViewModels | ⚠️ PARTIAL | Business logic yes, UI state no |
| String resources | ⚠️ PLANNED | Needs abstraction layer |
@@ -0,0 +1,163 @@
# Complete expect/actual Catalog
All 24 expect declarations in Amethyst quartz module with rationale.
| # | Name | Type | Purpose | Why Abstracted | Files |
|---|------|------|---------|----------------|-------|
| 1 | AESCBC | class | AES CBC encryption | Platform crypto APIs differ | quartz/.../ciphers/AESCBC.kt |
| 2 | AESGCM | class | AES GCM encryption | Platform crypto APIs differ | quartz/.../ciphers/AESGCM.kt |
| 3 | DigestInstance | class | Hash digests (SHA256) | Platform implementations | quartz/.../diggest/DigestInstance.kt |
| 4 | MacInstance | class | MAC (HMAC) operations | Platform crypto APIs | quartz/.../mac/MacInstance.kt |
| 5 | Sha256 | object | SHA256 hashing | Platform-specific optimizations | quartz/.../sha256/Sha256.kt |
| 6 | LargeCache | object | Large object caching | Platform storage APIs differ | quartz/.../cache/LargeCache.kt |
| 7 | UriParser | object | URI parsing | Platform URL APIs differ | quartz/.../UriParser.kt |
| 8 | UrlEncoder | object | URL encoding | Platform encoding differs | quartz/.../UrlEncoder.kt |
| 9 | Urls | object | URL utilities | Platform URL handling | quartz/.../Urls.kt |
| 10 | Platform | functions | platform(), currentTimeSeconds() | Platform name & time APIs | quartz/.../Platform.kt |
| 11 | Rfc3986 | object | RFC 3986 URL normalization | Used in jvmAndroid | quartz/.../Rfc3986.kt |
| 12 | Secp256k1Instance | object | Bitcoin crypto (secp256k1) | Different libs per platform | quartz/.../Secp256k1Instance.kt |
| 13 | SecureRandom | object | Cryptographically secure random | Platform random APIs differ | quartz/.../SecureRandom.kt |
| 14 | StringExt | functions | String utilities | Platform string handling | quartz/.../StringExt.kt |
| 15 | UnicodeNormalizer | object | Unicode normalization | Platform text APIs | quartz/.../UnicodeNormalizer.kt |
| 16 | GZip | object | GZip compression | Platform compression APIs | quartz/.../GZip.kt |
| 17 | LibSodiumInstance | object | NaCl/libsodium (NIP-44 encryption) | Different libs per platform | quartz/.../LibSodiumInstance.kt |
| 18 | Log | object | Logging | Platform logging systems | quartz/.../Log.kt |
| 19 | BigDecimal | class | Arbitrary precision decimal | Not in Kotlin common stdlib | quartz/.../BigDecimal.kt |
| 20 | BitSet | class | Bit set data structure | Not in Kotlin common stdlib | quartz/.../BitSet.kt |
| 21 | ServerInfoParser | object | Server info parsing (NIP-96) | Platform JSON parsing | quartz/.../nip96.../ServerInfoParser.kt |
| 22 | EventHasherSerializer | object | Event hashing | Platform-specific optimizations | quartz/.../nip01Core.../EventHasherSerializer.kt |
| 23 | OptimizedJsonMapper | object | JSON mapping | Platform JSON libraries | quartz/.../nip01Core.../OptimizedJsonMapper.kt |
| 24 | Address | data class | Address data structure | Platform-specific string handling | quartz/.../nip01Core.../Address.kt |
## Pattern Analysis
### Objects (Singletons) - 19 total
Most common pattern for platform-specific singletons:
- Crypto: Secp256k1Instance, LibSodiumInstance, Sha256
- I/O: UriParser, UrlEncoder, GZip
- Utils: Log, Platform, SecureRandom
### Classes (Instantiable) - 4 total
For objects that need to maintain state:
- AESCBC, AESGCM (cipher state)
- DigestInstance, MacInstance (hash/MAC state)
- BigDecimal, BitSet (data structures)
### Functions - 2 total
Simple utilities:
- platform(), currentTimeSeconds()
## Why Abstracted Categories
### Crypto (8 items)
**Always abstract:** Security APIs fundamentally different across platforms
- Android: Android Keystore, secp256k1-android
- Desktop: JVM crypto, secp256k1-jvm
- iOS: Security framework, native crypto
### I/O & Platform Utils (7 items)
**Often abstract:** File systems, URLs, compression differ
- Platform storage APIs
- URL handling varies
- Compression libraries differ
### Data Structures (2 items)
**Abstract when missing:** Not available in Kotlin common stdlib
- BigDecimal, BitSet not in common
### JSON/Parsing (3 items)
**Platform-specific optimization:** Uses platform JSON libraries
- Android/Desktop: Jackson (via jvmAndroid)
- iOS: Native parsers
### Logging (1 item)
**Always abstract:** Platform logging systems differ
- Android: android.util.Log
- Desktop: println or logging framework
- iOS: NSLog or OSLog
## Actual Implementation Examples
### Simple Object Pattern
```kotlin
// commonMain
expect object Log {
fun d(tag: String, message: String)
}
// androidMain
actual object Log {
actual fun d(tag: String, message: String) {
android.util.Log.d(tag, message)
}
}
// jvmMain
actual object Log {
actual fun d(tag: String, message: String) {
println("[$tag] $message")
}
}
```
### Complex Object with Dependencies
```kotlin
// commonMain
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}
// androidMain - uses JNI bindings
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
}
}
// jvmMain - different JNI library
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
}
}
// iosMain - native iOS implementation
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
// Uses iOS Security framework or native lib
}
}
```
### Class Pattern
```kotlin
// commonMain
expect class BigDecimal {
constructor(value: String)
fun add(other: BigDecimal): BigDecimal
override fun toString(): String
}
// jvmAndroid (works on Android + Desktop)
actual typealias BigDecimal = java.math.BigDecimal
// iosMain
actual class BigDecimal {
private val value: NSDecimalNumber
actual constructor(value: String) {
this.value = NSDecimalNumber(value)
}
// ... implementation
}
```
## Decision Patterns
Ask for each declaration:
1. **Used by 2+ platforms?** → YES (otherwise platform-specific)
2. **Pure Kotlin possible?** → NO (otherwise commonMain)
3. **Varies by platform?** → YES (expect/actual)
4. **JVM-only library?** → NO (otherwise jvmAndroid)
@@ -0,0 +1,332 @@
# Source Set Hierarchy in Amethyst
Visual guide to source set organization with concrete examples from the codebase.
## Hierarchy Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ Pure Kotlin, no platform APIs │
│ Examples: │
│ - Nostr event parsing (TextNoteEvent, MetadataEvent) │
│ - Business logic (data validation, crypto algorithms) │
│ - Data models (@Immutable data classes) │
│ Dependencies: kotlin-stdlib, kotlinx-coroutines │
└──────────────────────┬──────────────────────────────────────┘
┌────────────┴────────────┬───────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ jvmAndroid │ │ iosMain │ │ Future: │
│ JVM libraries │ │ iOS common │ │ jsMain │
│ Examples: │ │ Examples: │ │ wasmMain │
│ - Jackson JSON │ │ - Platform API │ └──────────────┘
│ - OkHttp HTTP │ │ - Actuals for │
│ - url-detector │ │ crypto/I/O │
│ Dependencies: │ │ Dependencies: │
│ - Jackson │ │ - Platform libs │
│ - OkHttp │ └───────┬───────────┘
└────┬─────────┬───┘ │
│ │ ├─→ iosX64Main (simulator Intel)
│ │ ├─→ iosArm64Main (device ARM64)
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
▼ ▼
┌──────────┐ ┌───────────┐
│android │ │ jvmMain │
│Main │ │ (Desktop) │
│Examples: │ │ Examples: │
│- Activity│ │- Window │
│- ViewModel│ │- MenuBar │
│- Android │ │- Desktop │
│ APIs │ │ Compose │
│Deps: │ │ Deps: │
│- secp256k│ │- secp256k │
│ 1-android│ │ 1-jvm │
│- androidx│ │- Compose │
│ │ │ Desktop │
└──────────┘ └───────────┘
```
## Dependency Flow
```
Code in commonMain
↓ can use
Nothing (only Kotlin stdlib)
Code in jvmAndroid
↓ can use
commonMain + JVM libraries (Jackson, OkHttp)
Code in androidMain
↓ can use
commonMain + jvmAndroid + Android framework
Code in jvmMain
↓ can use
commonMain + jvmAndroid + JVM + Compose Desktop
Code in iosMain
↓ can use
commonMain + iOS platform APIs
```
## Real Examples from Amethyst
### commonMain - Pure Kotlin
**File:** `quartz/src/commonMain/.../TextNoteEvent.kt`
```kotlin
@Immutable
class TextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseThreadedEvent(...) {
// Pure Kotlin - works everywhere
override fun indexableContent() = "Subject: " + subject() + "\n" + content
}
```
**Why commonMain:**
- Pure Kotlin code
- No platform APIs
- Data class with business logic
- Needed by all platforms
---
### jvmAndroid - JVM Libraries
**File:** `quartz/build.gradle.kts`
```kotlin
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
// Normalizes URLs
api(libs.rfc3986.normalizer)
// Performant Parser of JSONs into Events
api(libs.jackson.module.kotlin)
// Parses URLs from Text
api(libs.url.detector)
// Websockets API
implementation(libs.okhttp)
implementation(libs.okhttpCoroutines)
}
}
jvmMain { dependsOn(jvmAndroid) } // Desktop gets Jackson, OkHttp
androidMain { dependsOn(jvmAndroid) } // Android gets Jackson, OkHttp
```
**Why jvmAndroid:**
- Jackson, OkHttp are JVM-only libraries
- Works on Android (JVM) and Desktop (JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
**Usage in code:**
```kotlin
// Can use Jackson in jvmAndroid source set
val mapper = ObjectMapper()
val event = mapper.readValue(json, Event::class.java)
```
---
### androidMain - Android Platform
**File:** `amethyst/src/main/.../MainActivity.kt`
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // Android API
super.onCreate(savedInstanceState)
setContent { // Compose for Android
AmethystTheme {
val accountStateViewModel: AccountStateViewModel = viewModel()
AccountScreen(accountStateViewModel)
}
}
}
}
```
**Why androidMain:**
- AppCompatActivity is Android framework
- Activity lifecycle Android-specific
- AndroidX libraries (viewModel())
**Dependencies:**
```kotlin
androidMain {
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
dependencies {
implementation(libs.androidx.core.ktx)
api(libs.secp256k1.kmp.jni.android) // Android crypto
}
}
```
---
### jvmMain - Desktop Platform
**File:** `desktopApp/src/jvmMain/.../Main.kt`
```kotlin
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp
)
Window( // Compose Desktop API
onCloseRequest = ::exitApplication,
state = windowState,
title = "Amethyst"
) {
MenuBar { // Desktop-specific
Menu("File") {
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true))
}
}
NavigationRail { ... } // Sidebar
}
}
```
**Why jvmMain:**
- Window, MenuBar, NavigationRail are Compose Desktop
- Keyboard shortcuts desktop paradigm
- Different UX from Android (sidebar vs bottom nav)
**Dependencies:**
```kotlin
jvmMain {
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm) // Desktop crypto
implementation(compose.desktop.currentOs)
}
}
```
---
### iosMain - iOS Platform
**File:** `quartz/build.gradle.kts`
```kotlin
iosMain {
dependsOn(commonMain.get())
dependencies {
// iOS platform dependencies
}
}
val iosX64Main by getting { dependsOn(iosMain.get()) }
val iosArm64Main by getting { dependsOn(iosMain.get()) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
```
**Why iosMain:**
- iOS platform APIs
- Native crypto (Security framework)
- Different from Android/Desktop
**Architecture targets:**
- iosX64Main: Intel simulator
- iosArm64Main: Device (iPhone, iPad)
- iosSimulatorArm64Main: Apple Silicon simulator
---
## Build Order Matters
**CRITICAL:** jvmAndroid must be defined BEFORE androidMain and jvmMain:
```kotlin
// ✅ CORRECT ORDER
val jvmAndroid = create("jvmAndroid") { ... }
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
// ❌ WRONG - Build error
androidMain { dependsOn(jvmAndroid) } // jvmAndroid not defined yet!
val jvmAndroid = create("jvmAndroid") { ... }
```
See comment in quartz/build.gradle.kts:131:
```kotlin
// Must be defined before androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") { ... }
```
## Choosing the Right Source Set
Decision flowchart:
```
Q: Where should this code go?
├─ Pure Kotlin? (no platform APIs)
│ └─ commonMain
├─ JVM library? (Jackson, OkHttp)
│ └─ jvmAndroid
├─ Android API? (Activity, Context)
│ └─ androidMain
├─ Desktop API? (Window, MenuBar)
│ └─ jvmMain
└─ iOS API? (platform.posix, Security)
└─ iosMain
```
## Future: Web/wasm Source Sets
**Not yet implemented**, but structure would be:
```
commonMain
├─→ jsMain (JavaScript/Web)
│ └─ JS-specific: DOM APIs, fetch
└─→ wasmMain (WebAssembly)
└─ wasm-specific: limited APIs
```
**Constraints:**
- Cannot use jvmAndroid (Jackson, OkHttp)
- Cannot use platform.posix
- Must use pure Kotlin or web-compatible libs (ktor, kotlinx.serialization)
## Summary Table
| Source Set | Extends | Can Use | Example Code |
|------------|---------|---------|--------------|
| commonMain | - | Kotlin stdlib only | TextNoteEvent, business logic |
| jvmAndroid | commonMain | JVM libs (Jackson, OkHttp) | JSON parsing, HTTP |
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
| iosMain | commonMain | iOS platform | Security framework |
| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific |
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
| jsMain | commonMain | JS/DOM | Web (future) |
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |
@@ -0,0 +1,345 @@
# Target Compatibility Guide
Current targets (Android, JVM/Desktop, iOS) and future targets (web, wasm) with constraints.
## Current Primary Targets
### Android (androidMain)
**Status:** ✅ Mature, production-ready
**Runtime:** JVM (Dalvik/ART)
**Available:**
- Android framework (Activity, Context, Intent, etc.)
- AndroidX libraries (ViewModel, Navigation, etc.)
- JVM libraries via jvmAndroid (Jackson, OkHttp)
- Platform-specific crypto: secp256k1-kmp-jni-android
**Constraints:**
- Mobile UX paradigms (bottom navigation, vertical scroll)
- Touch-first interaction
- Limited screen space
- Battery/performance constraints
**Example code:**
```kotlin
// androidMain
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Android-specific lifecycle
}
}
```
---
### JVM / Desktop (jvmMain)
**Status:** ✅ Active development, functional
**Runtime:** JVM
**Available:**
- Pure JVM libraries
- JVM libraries via jvmAndroid (Jackson, OkHttp)
- Compose Desktop (Window, MenuBar, etc.)
- Platform-specific crypto: secp256k1-kmp-jni-jvm
**Constraints:**
- Desktop UX paradigms (sidebar, menus, keyboard shortcuts)
- Keyboard + mouse interaction
- Larger screen space
- Different navigation patterns (no back stack)
**Example code:**
```kotlin
// jvmMain
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Amethyst"
) {
MenuBar { ... } // Desktop-specific
NavigationRail { ... } // Sidebar
}
}
```
---
### iOS (iosMain + architecture targets)
**Status:** ⚠️ In development, framework configured
**Runtime:** Native iOS
**Source sets:**
- iosMain (common iOS code)
- iosX64Main (Intel simulator)
- iosArm64Main (device - iPhone/iPad)
- iosSimulatorArm64Main (Apple Silicon simulator)
**Available:**
- iOS platform APIs (platform.posix, Foundation, etc.)
- Native crypto (Security framework)
- SwiftUI integration (via KMP framework)
**NOT available:**
- JVM libraries (Jackson, OkHttp)
- jvmAndroid source set
- JVM-specific APIs
**Constraints:**
- Mobile UX (similar to Android)
- Swift/Objective-C interop
- XCFramework distribution
- CocoaPods integration
**Example code:**
```kotlin
// iosMain
actual object Secp256k1Instance {
actual fun signSchnorr(...): ByteArray {
// Use iOS Security framework
}
}
```
**XCFramework setup:**
```kotlin
// quartz/build.gradle.kts
kotlin {
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
.forEach { target ->
target.binaries.framework {
baseName = "quartz-kmpKit"
isStatic = true
}
}
}
```
---
## Future Targets
### Web / JavaScript (jsMain)
**Status:** ❌ Not implemented, consider for future
**Runtime:** JavaScript (browser or Node.js)
**Available:**
- Kotlin/JS stdlib
- JS/DOM APIs
- kotlinx.* libraries (serialization, coroutines, datetime)
- ktor-client (HTTP)
**NOT available:**
- ❌ JVM libraries (Jackson, OkHttp)
- ❌ jvmAndroid source set
- ❌ platform.posix (no file system access like native)
- ❌ Blocking APIs (different async model - JS event loop)
**Constraints:**
- Single-threaded event loop
- No blocking calls
- Different async patterns (Promises, async/await)
- Browser security (CORS, no file system)
**Migration path from current code:**
| Current (jvmAndroid) | Web-compatible alternative |
|---------------------|---------------------------|
| Jackson JSON | kotlinx.serialization |
| OkHttp HTTP | ktor-client |
| java.math.BigDecimal | Kotlin BigDecimal (coming) |
| Blocking I/O | Suspending functions |
**Example migration:**
```kotlin
// Current: jvmAndroid
val mapper = ObjectMapper()
val event = mapper.readValue(json, Event::class.java)
// Future: commonMain (works on web)
val json = Json { ignoreUnknownKeys = true }
val event = json.decodeFromString<Event>(jsonString)
```
---
### WebAssembly (wasmMain)
**Status:** ❌ Not implemented, experimental Kotlin/Wasm
**Runtime:** WebAssembly
**Available:**
- Kotlin/Wasm stdlib
- Limited kotlinx.* libraries
- wasm-specific APIs
**NOT available:**
- ❌ JVM libraries
- ❌ Full platform.posix
- ❌ Many kotlinx libraries (limited wasm support)
**Constraints:**
- Even more limited than JS
- Experimental Kotlin support
- Limited library ecosystem
**Recommendation:** Focus on web (jsMain) first, wasm later.
---
## Cross-Target Compatibility Matrix
| Feature | Android | JVM/Desktop | iOS | Web (JS) | wasm |
|---------|---------|-------------|-----|----------|------|
| Pure Kotlin | ✅ | ✅ | ✅ | ✅ | ✅ |
| kotlinx.coroutines | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| kotlinx.serialization | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| kotlinx.datetime | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| ktor-client | ✅ | ✅ | ✅ | ✅ | ❌ |
| Jackson JSON | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
| OkHttp | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
| platform.posix | ❌ | ❌ | ✅ | ❌ | ⚠️ |
| Compose Multiplatform | ✅ | ✅ | ⚠️ (experimental) | ⚠️ (experimental) | ❌ |
Legend:
- ✅ Full support
- ⚠️ Limited/experimental
- ❌ Not available
---
## Future-Proofing Recommendations
### For Web Compatibility
**DO:**
- ✅ Use kotlinx.serialization instead of Jackson
- ✅ Use ktor-client instead of OkHttp
- ✅ Use kotlinx.datetime instead of java.time
- ✅ Use suspending functions (non-blocking)
- ✅ Keep business logic in commonMain
**DON'T:**
- ❌ Put JVM libraries in commonMain
- ❌ Use platform.posix for critical features
- ❌ Use blocking I/O
- ❌ Depend on threading (use coroutines)
**Example:**
```kotlin
// ❌ NOT web-compatible
// jvmAndroid
fun parseJson(json: String): Event {
val mapper = ObjectMapper() // Jackson - JVM only
return mapper.readValue(json, Event::class.java)
}
// ✅ Web-compatible
// commonMain
@Serializable
data class Event(...)
fun parseJson(json: String): Event {
return Json.decodeFromString<Event>(json) // Works everywhere
}
```
### Current Migration Priorities
**High priority:** (Needed for web)
1. Migrate Jackson → kotlinx.serialization
2. Migrate OkHttp → ktor-client
3. Move business logic to commonMain
**Medium priority:** (Nice to have)
1. Abstract date/time handling → kotlinx.datetime
2. Remove platform.posix usage where possible
3. Use suspending functions over blocking
**Low priority:** (Future optimization)
1. wasm-specific optimizations
2. Platform-specific performance tuning
---
## Platform-Specific Patterns
### Android vs iOS Differences
| Aspect | Android | iOS |
|--------|---------|-----|
| **Activity/ViewController** | Activity | UIViewController |
| **Navigation** | Compose Navigation | UINavigationController |
| **Lifecycle** | onCreate, onResume, etc. | viewDidLoad, viewWillAppear |
| **Permissions** | Runtime permissions | Info.plist + runtime |
| **Crypto** | secp256k1-android | Security framework |
| **Storage** | Room, SharedPreferences | Core Data, UserDefaults |
### Desktop vs Mobile Differences
| Aspect | Desktop | Mobile |
|--------|---------|--------|
| **Navigation** | Sidebar | Bottom nav |
| **Input** | Keyboard + mouse | Touch |
| **Screen** | Large, landscape | Small, portrait |
| **Windows** | Multi-window | Single app |
| **Shortcuts** | Keyboard shortcuts (Ctrl+N) | None |
| **Menus** | MenuBar | Bottom sheets |
---
## Testing Strategy
### Per-Target Testing
**Android:**
- Unit tests: androidTest
- Instrumented: androidInstrumentedTest
- Device/emulator testing
**Desktop:**
- Unit tests: jvmTest
- Manual desktop app testing
**iOS:**
- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.)
- Simulator/device testing
**Web (future):**
- Unit tests: jsTest
- Browser testing (Selenium, Playwright)
### Shared Testing
**commonTest:**
- Business logic tests
- Pure Kotlin code
- Works on all platforms
```kotlin
// commonTest
class EventParsingTest {
@Test
fun parseTextNoteEvent() {
// Tests run on all platforms
}
}
```
---
## Summary
**Current Focus:** Android, JVM/Desktop, iOS (active development)
**Future Considerations:** Web (requires migration from Jackson/OkHttp)
**Key Decision:** Prefer kotlinx.* libraries over JVM-specific libs for future web compatibility.
@@ -0,0 +1,166 @@
#!/bin/bash
# Suggests KMP library alternatives for JVM-specific dependencies
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "=== KMP Dependency Suggestions ==="
echo
# Colors
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
SUGGESTIONS_FOUND=0
# Check for Jackson (suggest kotlinx.serialization)
echo "📦 Checking for Jackson JSON..."
if grep -r "jackson" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
echo -e "${YELLOW}⚠ Found Jackson dependency${NC}"
echo " Current: Jackson (JVM-only)"
echo -e " ${GREEN}Suggest: kotlinx.serialization${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Remove:"
echo " api(libs.jackson.module.kotlin)"
echo
echo " // Add to commonMain:"
echo " implementation(libs.kotlinx.serialization.json)"
echo
echo " // Code change:"
echo " // Before (Jackson):"
echo " val mapper = ObjectMapper()"
echo " val event = mapper.readValue(json, Event::class.java)"
echo
echo " // After (kotlinx.serialization):"
echo " @Serializable"
echo " data class Event(...)"
echo " val event = Json.decodeFromString<Event>(json)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using Jackson (or already using kotlinx.serialization)${NC}"
fi
# Check for OkHttp (suggest ktor)
echo
echo "📦 Checking for OkHttp..."
if grep -r "okhttp" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
echo -e "${YELLOW}⚠ Found OkHttp dependency${NC}"
echo " Current: OkHttp (JVM-only)"
echo -e " ${GREEN}Suggest: ktor-client${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Remove:"
echo " implementation(libs.okhttp)"
echo
echo " // Add to commonMain:"
echo " implementation(libs.ktor.client.core)"
echo " // Platform-specific engines:"
echo " // androidMain: implementation(libs.ktor.client.android)"
echo " // jvmMain: implementation(libs.ktor.client.cio)"
echo " // iosMain: implementation(libs.ktor.client.darwin)"
echo
echo " // Code change:"
echo " // Before (OkHttp):"
echo " val client = OkHttpClient()"
echo " val request = Request.Builder().url(url).build()"
echo " val response = client.newCall(request).execute()"
echo
echo " // After (ktor):"
echo " val client = HttpClient()"
echo " val response: String = client.get(url)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using OkHttp (or already using ktor)${NC}"
fi
# Check for java.time (suggest kotlinx.datetime)
echo
echo "📦 Checking for java.time usage..."
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.time\." >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found java.time imports${NC}"
echo " Current: java.time (JVM-only)"
echo -e " ${GREEN}Suggest: kotlinx.datetime${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Add to commonMain:"
echo " implementation(libs.kotlinx.datetime)"
echo
echo " // Code change:"
echo " // Before (java.time):"
echo " import java.time.Instant"
echo " val now = Instant.now()"
echo
echo " // After (kotlinx.datetime):"
echo " import kotlinx.datetime.Clock"
echo " val now = Clock.System.now()"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using java.time (or already using kotlinx.datetime)${NC}"
fi
# Check for java.math.BigDecimal
echo
echo "📦 Checking for java.math.BigDecimal usage..."
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.math.BigDecimal" >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found java.math.BigDecimal imports${NC}"
echo " Current: java.math.BigDecimal (JVM-only)"
echo -e " ${BLUE}Note:${NC} KMP BigDecimal not yet in stable kotlinx"
echo
echo " Options:"
echo " 1. Use expect/actual (current approach in quartz)"
echo " 2. Wait for kotlinx.decimal (proposal stage)"
echo " 3. Use third-party KMP library (e.g., bignum)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using java.math.BigDecimal directly${NC}"
fi
# Check for platform.posix usage
echo
echo "📦 Checking for platform.posix usage..."
if find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "import platform.posix\." >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found platform.posix in commonMain${NC}"
echo " Current: platform.posix (native platforms only, not web)"
echo -e " ${GREEN}Suggest:${NC} Abstract file I/O with expect/actual"
echo
echo " For web compatibility:"
echo " - iOS/Native: platform.posix"
echo " - Web: Use kotlinx-io or ktor file APIs"
echo " - Create expect/actual for file operations"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using platform.posix in commonMain${NC}"
fi
# Summary
echo
echo "=== Summary ==="
if [ "$SUGGESTIONS_FOUND" -eq 0 ]; then
echo -e "${GREEN}✓ No JVM-specific dependencies found!${NC}"
echo " Your code is ready for web/wasm targets."
else
echo -e "${YELLOW}Found $SUGGESTIONS_FOUND suggestion(s) for KMP alternatives${NC}"
echo
echo "Priority recommendations:"
echo " 1. ${GREEN}High:${NC} Jackson → kotlinx.serialization (enables web support)"
echo " 2. ${GREEN}High:${NC} OkHttp → ktor-client (enables web support)"
echo " 3. ${GREEN}Medium:${NC} java.time → kotlinx.datetime"
echo " 4. ${GREEN}Low:${NC} Consider web compatibility for platform.posix usage"
echo
echo "Resources:"
echo " - kotlinx.serialization: https://github.com/Kotlin/kotlinx.serialization"
echo " - ktor: https://ktor.io/docs/client.html"
echo " - kotlinx.datetime: https://github.com/Kotlin/kotlinx-datetime"
fi
exit 0
@@ -0,0 +1,126 @@
#!/bin/bash
# Validates KMP source set structure and detects common issues
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "=== Validating KMP Structure ==="
echo
# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
ISSUES_FOUND=0
# Check 1: jvmAndroid defined before androidMain/jvmMain
echo "📋 Checking source set definition order..."
if [ -f "quartz/build.gradle.kts" ]; then
jvmandroid_line=$(grep -n "val jvmAndroid = create" quartz/build.gradle.kts | cut -d: -f1)
android_line=$(grep -n "androidMain {" quartz/build.gradle.kts | cut -d: -f1)
jvm_line=$(grep -n "jvmMain {" quartz/build.gradle.kts | cut -d: -f1)
if [ -n "$jvmandroid_line" ] && [ -n "$android_line" ] && [ -n "$jvm_line" ]; then
if [ "$jvmandroid_line" -lt "$android_line" ] && [ "$jvmandroid_line" -lt "$jvm_line" ]; then
echo -e "${GREEN}${NC} jvmAndroid defined before androidMain and jvmMain"
else
echo -e "${RED}${NC} jvmAndroid must be defined BEFORE androidMain and jvmMain"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
fi
fi
fi
# Check 2: Platform code in commonMain (Android imports)
echo
echo "📋 Checking for platform code in commonMain..."
android_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import android\." || true)
if [ -n "$android_imports_in_common" ]; then
echo -e "${RED}${NC} Found Android imports in commonMain:"
echo "$android_imports_in_common" | sed 's/^/ /'
echo " Fix: Move to androidMain or create expect/actual"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
else
echo -e "${GREEN}${NC} No Android imports in commonMain"
fi
# Check 3: JVM libraries in commonMain (Jackson, OkHttp)
echo
echo "📋 Checking for JVM libraries in commonMain..."
jvm_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import com.fasterxml.jackson\|^import okhttp3\." || true)
if [ -n "$jvm_imports_in_common" ]; then
echo -e "${RED}${NC} Found JVM library imports in commonMain:"
echo "$jvm_imports_in_common" | sed 's/^/ /'
echo " Fix: Move to jvmAndroid or migrate to kotlinx.serialization/ktor"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
else
echo -e "${GREEN}${NC} No JVM library imports in commonMain"
fi
# Check 4: Unmatched expect/actual declarations
echo
echo "📋 Checking expect/actual pairs..."
expect_files=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^expect " || true)
if [ -n "$expect_files" ]; then
for file in $expect_files; do
# Extract declarations
expects=$(grep "^expect \(class\|object\|fun\|interface\)" "$file" | sed 's/expect //' | awk '{print $2}' | sed 's/[({].*$//')
# Check for actuals in platform source sets
for expect_name in $expects; do
actual_count=0
for platform in androidMain jvmMain iosMain; do
platform_dir=$(dirname "$file" | sed "s/commonMain/$platform/")
platform_file="${platform_dir}/$(basename "$file")"
if [ -f "$platform_file" ] && grep -q "actual.*$expect_name" "$platform_file"; then
actual_count=$((actual_count + 1))
fi
done
if [ "$actual_count" -eq 0 ]; then
echo -e "${YELLOW}${NC} No actual implementations found for: $expect_name in $file"
echo " Check: androidMain, jvmMain, iosMain"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
fi
done
done
else
echo -e "${GREEN}${NC} No expect declarations to validate"
fi
# Check 5: Duplicated business logic across platforms
echo
echo "📋 Checking for potential code duplication..."
# This is a heuristic check - look for similar function names in different platform source sets
common_functions=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -h "^fun " | awk '{print $2}' | sed 's/[({<].*$//' | sort -u || true)
if [ -n "$common_functions" ]; then
for func in $common_functions; do
android_count=$(find */src/androidMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
jvm_count=$(find */src/jvmMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
if [ "$android_count" -gt 0 ] && [ "$jvm_count" -gt 0 ]; then
echo -e "${YELLOW}${NC} Function '$func' found in both androidMain and jvmMain"
echo " Consider: Move to commonMain or jvmAndroid if truly shared"
fi
done
fi
# Summary
echo
echo "=== Summary ==="
if [ "$ISSUES_FOUND" -eq 0 ]; then
echo -e "${GREEN}✓ All checks passed!${NC}"
exit 0
else
echo -e "${RED}✗ Found $ISSUES_FOUND issue(s)${NC}"
echo
echo "Common fixes:"
echo " 1. Platform code in commonMain → Move to androidMain or create expect/actual"
echo " 2. JVM libraries in commonMain → Move to jvmAndroid or migrate to kotlinx.*"
echo " 3. Missing actual implementations → Implement in all target platforms"
echo " 4. Duplicated logic → Move to commonMain or jvmAndroid"
exit 1
fi