Files
amethyst/.claude/skills/amy-expert/references/extraction-recipe.md
Vitor PamplonaandClaude Opus 5.5 395af4dc9c chore(skills): consolidate 33 skills into 14, verified against the tree
The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list
budget (2% of the context window, or 8,000 chars when unknown), and a review
found most bodies stale: paths from before the commons/commonsUI moves, the
deleted legacy desktop app, a callback signer API that no longer exists, and
example APIs that were never in the tree.

Kept, rewritten and checked against the code (every path, class and command):
account-state (+feeds), amy-expert, android-expert, auth-signers,
compose-expert (+API style, Navigation 3 back stacks), desktop-expert
(+run/package), event-store-semantics (+NIP-50 search), find-missing-
translations, kotlin-coroutines (+structured concurrency, flow modeling),
kotlin-multiplatform (+Gradle, composable extraction), ngit-pr,
nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog),
relay-client.

Dropped: textbook Compose/Kotlin technique skills, kotlin-expert,
kotlin-types-value-class, gradle-expert, quartz-integration (external-facing;
see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is
now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands.
The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md
(stale history) and repoints SearchableEvent's KDoc to event-store-semantics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:21:01 -04:00

135 lines
5.5 KiB
Markdown

# 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
```bash
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
```bash
# Target location depends on what it is:
# - Protocol → quartz/src/commonMain/kotlin/…
# - 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.