Files
amethyst/.claude/skills/amy-expert/references/extraction-recipe.md
davotoula 03bf1771e0 docs: drop in-repo quartz/quic module references after the library split
quartz and quic now come from Maven Central (libs.quartz, libs.quic), but a
few module docs and skills still described them as Gradle modules here:

- marmotQuic/README.md, nestsClient/tests/README.md: `:quic` -> quic-kmp
- cli/DEVELOPMENT.md, commonsUI/ARCHITECTURE.md: `:quartz` -> libs.quartz
- amy-expert / kotlin-multiplatform extraction recipes: protocol code is a
  quartz-repo PR (built here with -PquartzDir), not a git mv into quartz/
- AGENTS.md Tech Stack and the kotlin-multiplatform skill: the app modules
  moved to kotlinx.serialization; Jackson remains only inside quartz's JVM
  code, so the "don't migrate to kotlinx.serialization" rule is reversed
- BUILDING.md: the currentTimeMillis ban is verifyKmpPurity, not a Spotless rule
2026-10-22 15:57:32 +02:00

5.6 KiB

Extract-from-Android recipe

The single most common reason an Amy feature request stalls: the logic it needs lives in amethyst/ with Android-only imports. You cannot call it from cli/. You have to move it first.

This recipe is how.

When to extract

Before writing a new command, ask:

  1. Does the piece of logic I need exist in quartz/ or commons/?

    • Yes → use it.
    • No, but it's in amethyst/ → extract. This file.
    • No, it doesn't exist anywhere → design it in commons/ directly. Write a plan doc under commons/plans/ if it's a new subsystem.
  2. Never duplicate amethyst/ logic into cli/. That's a debt you will pay later when the Android caller drifts.

Recipe

Land this as its own commit, before the commit that adds the CLI command.

Step 1 — Find the class

grep -rn "class <ClassName>" amethyst/src/main/java/

Identify the minimum unit to move. Sometimes it's a whole file, sometimes one function. Prefer the smallest unit that makes the command possible.

Step 2 — List Android-only dependencies

Walk the imports. The usual offenders:

Dependency Treatment
android.content.Context Often accidental — inline if only used for logging or preferences. Otherwise, invert as constructor arg.
android.content.SharedPreferences Abstract behind an interface in commons/; Android actual uses SharedPreferences, JVM actual uses a JSON file.
androidx.work.WorkManager Rarely shareable — if the CLI needs it, simplify the flow to not require background scheduling.
android.util.Log Replace with Quartz Log (quartz/…/utils/Log.kt), lambda overload for interpolated messages.
android.graphics.Bitmap Almost never needed by Amy. Keep in Android and split the function.
android.net.Uri Replace with kotlinx.io path types or a plain String.
androidx.compose.* Compose UI (ui/foundation/material3), Coil and Res must stay out of commons entirely — they belong in :commonsUI, which Amy never depends on. Only the Compose runtime (@Stable, snapshot state) is allowed in commons.

Step 3 — Pick a migration strategy per dependency

  • Inline-able. One call, trivial. Delete it.
  • Platform-abstractable. Add expect in commons/commonMain/ + actual in commons/androidMain/ + actual in commons/jvmMain/. See the kotlin-multiplatform skill for the mechanics and for the jvmAndroid source-set pattern used throughout this repo.
  • Inversion-of-control. Take the Android dependency as a constructor arg with an interface type. Amy supplies a JVM flavour; Android supplies the Context-backed one.

Step 4 — Move the code

# Target location depends on what it is:
# - Protocol → a PR to vitorpamplona/quartz (quartz/src/commonMain/kotlin/… there),
#   built here with -PquartzDir=../quartz until it ships; not a git mv in this repo
# - Business logic → commons/src/commonMain/kotlin/…
# - UI → commonsUI/src/commonMain/… (needs Compose Multiplatform; never used by amy)
git mv amethyst/src/main/java/com/.../<ClassName>.kt \
       commons/src/commonMain/kotlin/com/.../<ClassName>.kt

Update package declarations. Run ./gradlew spotlessApply.

Step 5 — Update the Android caller

The amethyst/ caller now imports from the new location. Often this is the only code change visible in the Android app.

If the Android caller was using a concrete Android-backed dependency, it now supplies that concrete dependency explicitly.

Step 6 — Add a JVM test

In commons/src/commonTest/kotlin/… or commons/src/jvmTest/kotlin/… (depending on what the code exercises), add a test that runs on JVM. This guards against Android-only imports sneaking back in, and it's the only way to be sure Amy can now call the code.

Step 7 — Commit

Single commit, descriptive:

refactor(<area>): extract <ClassName> to commons for CLI reuse, with a body naming the old and new location, how each Android dependency was handled (inlined, expect/actual, or inverted behind an interface), and "No behaviour change on Android."

Step 8 — Now add the CLI command

Separate commit. Follows "How to add a command" in cli/DEVELOPMENT.md.

Cautionary notes

  • Don't extract speculatively. Only extract what the current command needs. A feature-complete port can happen later; right now the goal is to unblock one command without adding surface area you don't have a second caller for.
  • Android is allowed to keep side-effects. Notifications, background services, Intents, camera, permissions dialogs — those stay in amethyst/. Amy's job isn't to replicate UX, it's to exercise the protocol underneath.
  • Check the consumers. Sometimes the "logic" you want is already partially in commons/, and the amethyst/ class is just a thin wrapper. In that case, re-use the commons/ class directly and delete the wrapper or keep it if Android genuinely needs it.
  • Tests first if you're nervous. Copy the existing amethyst/-side test (if any), make it JVM-only by removing Android imports, and watch it pass after the move.

Red flags during extraction

Stop and reconsider if:

  • The Android class is 1000+ lines. Extract only the piece the CLI needs; leave the rest for a follow-up.
  • You need Context in 30 places. It's probably being used as a grab-bag; sort by actual use (strings, preferences, services, …) and abstract those individually.
  • You find yourself writing expect class with a dozen methods. A fine-grained interface is usually clearer than a monolithic expect-actual.
  • You're about to add a Compose import to cli/. Stop.