Files
com.vitorpamplona.amethyst/commons/plans/2026-05-30-amethyst-to-commons-migration.md
Claude 6579b5f656 docs: audit, status-stamp, and index all module plans
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.

Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:

  shipped (archived): 122   in-progress: 8   queued: 7   abandoned: 4

docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
2026-06-30 15:35:38 +00:00

316 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Amethyst → Commons Migration Plan
> **Status:** queued — the keystone Phase A is untouched: no `Account.kt`/`LocalCache.kt` engine in commons, the amethyst monoliths remain (3757 + 4092 LOC), and `commons/model/account` still holds only the pre-existing `AccountInfo/AccountStorage/SignerType` scaffolding the plan's §0 marks as already-present.
> _Audited 2026-06-30._
**Date:** 2026-05-30 · **Owning module:** `commons` · **Status:** plan (no code moved yet)
Goal: move the **shared** parts of the `amethyst` Android app down into `commons`
so Desktop, the headless `cli`, and future iOS reuse them, leaving `amethyst`
as a thin Android-native shell (Activity, navigation, notifications, platform
services). This is the inverse direction of the `commons` package refactor that
preceded it (see `commons/ARCHITECTURE.md`) — that cleaned up *where* shared
code lives; this plan decides *what* still needs to move there.
## Scope surveyed
`amethyst/src/main`**1817 Kotlin files**. Coupling signals:
| Signal | Files | Meaning |
|---|---|---|
| `androidx.compose.*` | 1075 | UI |
| `R.string` / `amethyst.R` | 559 | localized strings — see §"Strings" |
| `android.content.Context` | 142 | platform handle to abstract |
| Activity / Service / `android.app` | 40 | clearly stays native |
| `com.fasterxml.jackson` | 6 | purity-gate only matters for commonMain/iOS |
Three structured passes (model/, service/, ui/) produced the matrices below.
---
## 0. What is ALREADY in commons (do NOT re-extract)
The agents that surveyed `amethyst` did not all know commons' current state.
These already exist and the amethyst copies are **dedup / re-point** targets,
not new extractions:
- **Account scaffolding:** `model/IAccount`, `model/account/{AccountInfo,
AccountStorage,SignerType}`, `state/{FollowState,UserMetadataState,
LoadingState,EventCollectionState}`, `model/cache/*`. **But the concrete
`Account.kt` (3565 LOC) and `LocalCache.kt` (3917 LOC) are NOT extracted** —
only their interfaces/scaffolding are. They are the keystone (§Phase A).
- **Relay layer:** `relayClient/*` (assemblers, eoseManagers, composeSubscription
managers, preload, subscriptions, nip17Dm) and `relays/*`.
- **Formatters / util:** `util/PubKeyFormatter`, `util/NumberFormatters`,
`util/PlatformNumberFormatter` (expect/actual). amethyst's
`ui/note/PubKeyFormatter.kt` is a **duplicate**.
- **Services:** `service/lnurl/{LightningAddressResolver,OkHttpLnurlEndpointResolver}`,
`service/nwc`, `service/upload`. amethyst's `service/lnurl/LightningAddressResolver.kt`
is a **duplicate**.
- **Strings:** commons localizes through **Compose Multiplatform Resources**
(`composeResources/values/strings.xml`, `stringResource(Res.string.x)`),
already 41 strings. ← This is the migration path for `R.string`, NOT a custom
`StringProvider`.
- **Image loading:** Coil 3 is already KMP — `coil.compose` in `commonMain`,
`coil.okhttp` in `jvmAndroid`. Shared composables can call `AsyncImage`
directly; **image loading is largely a non-blocker**.
- **Misc:** `tor/`, `keystorage/` (interface + actuals), `richtext/`,
`preview/` (OpenGraph/MetaTags parsers), `feeds/`, `ui/feeds` (feed DAL).
---
## 1. Cross-cutting blockers (the real Phase 0)
Ranked by how much extraction they unblock. Note the corrections vs. the raw
survey (Coil and Strings are *less* of a blocker than assumed; account state is
*more*).
| Blocker | Reality | Resolution |
|---|---|---|
| **Account state** (`Account.kt`, `LocalCache.kt`, `AccountViewModel`) | The dominant blocker. ~53 UI files read it; nearly every ViewModel depends on it. commons has only the interfaces. | Extract `Account`/`LocalCache` business logic to `commons/model/account` + `model/cache`, behind the existing `IAccount`/cache interfaces. Keep Android-only bits (LocationState wiring, `@Stable`) in a thin adapter. **Keystone — §Phase A.** |
| **`R.string`** (559) | Solved pattern already in use. | Migrate needed strings into `commons/.../composeResources/values/strings.xml`; replace `R.string.x` → `Res.string.x`. No new abstraction. Crowdin config already exists at repo root. |
| **`Context`** (142) | Mostly file pick / clipboard / intents / cacheDir. | Small `expect`/`actual` platform-service interfaces (file picker, clipboard, URI open). Most are confined to `ui/actions/uploads` and a few service packages. |
| **`INav`** (nav callbacks) | Already an interface; low friction. | Shared composables take `onNavigate: (route) -> Unit` lambdas, not `INav` directly. |
| **Image loading (Coil)** | Already KMP in commons. | Use commons Coil directly; only the Android `ImageLoaderSetup`/fetchers stay native (or move to `jvmAndroid`). |
| **OkHttp / Jackson** | Allowed in `jvmAndroid`; banned only in `commonMain`/iOS. | HTTP/JSON code → `jvmAndroid`. Only promote to `commonMain` (for iOS) by swapping to Ktor + kotlinx.serialization, and only when iOS needs it. |
---
## 2. model/ — migration matrix
Most of `amethyst/model` is CLI-safe Nostr state and extracts cleanly. Verdicts:
**Extract** (pure) · **Abstract** (needs storage/HTTP/keystore seam) ·
**Keystone** · **Native**.
| Area | Verdict | Destination | Note |
|---|---|---|---|
| `Account.kt`, `LocalCache.kt`, `accountsCache/` | **Keystone** | `model/account`, `model/cache` | 3565+3917 LOC; behind `IAccount`/cache ifaces; LocationState → adapter |
| `AccountSettings`, `AccountSyncedSettings*` | Extract | `model/account` | pure data + event (de)serialization |
| `UiSettings`, `UiSettingsFlow` | Abstract | `model/` | drop `R.string` (use string keys / `Res`) |
| `nip01UserMetadata`, `nip02FollowLists`, `nip17Dms`, `nip30CustomEmojis`, `nip47WalletConnect`, `nip51Lists`, `nip65RelayList`, `nip72Communities`, `nip78AppSpecific`, `nip86RelayManagement`, `nipA3PaymentTargets`, `nipB7Blossom`, `nipBCOnchainZaps`, `trustedAssertions`, `zap` | Extract | `model/nipNN<slug>` (mirror quartz) | pure event-state wrappers; bulk move |
| `topNavFeeds/`, `algoFeeds/`, `serverList/` | Extract | `feeds/`, `model/relayLists` | feed query orchestration is platform-agnostic |
| `edits/` | Extract | `model/nip51Lists` or `model/nip37Drafts` | encrypted list/draft state |
| `preferences/` (DataStore, KeyStoreEncryption, *SharedPreferences) | Abstract | `keystorage/`, `model/preferences` | schemas → commons; persistence actuals stay Android |
| `marmot/` (Android*Store) | Abstract | `keystorage/marmot` + commons logic | KeyStore-backed stores → expect/actual |
| `nip03Timestamp`, `nip11RelayInfo`, `privacyOptions` (OkHttp), `localRelays` | Abstract | `jvmAndroid` (HTTP) + commonMain (logic) | OkHttp → jvmAndroid; ContentResolver stays native |
| `torState/` | Extract | `tor/`, `model/torState` | relay-eval logic is pure |
| `EncryptedStorage`, `LocalPreferences` | Abstract | `keystorage/` | core logic + iface; SharedPrefs impl stays Android |
| `Amethyst.kt` (Application), `AppModules.kt` (DI) | **Native** | — | Android lifecycle / Context assembly |
**Highest value, lowest friction:** the ~16 `nipNN` state packages + `topNavFeeds`
(bulk pure moves), then `AccountSettings`/`AccountSyncedSettings`.
---
## 3. service/ — migration matrix
| Package (files) | Verdict | Destination | Note |
|---|---|---|---|
| `relayClient` (81) | **Native** | — (dedup) | Android orchestration wiring over commons `relayClient`; do NOT duplicate. Lift any LocalCache-free EOSE managers into commons. |
| `playback` (60) | Native | — | ExoPlayer/Media3, PIP — Android-only |
| `notifications` (15) | Native | — | foreground service, NotificationManager |
| `okhttp` (17) | Extract | `jvmAndroid` | OkHttp clients/interceptors/WS bridge; already JVM-shaped |
| `uploads` (25) | Abstract | `jvmAndroid` + commonMain iface | compression/codecs stay Android; orchestration → common |
| `images` (10) | Abstract | `jvmAndroid` (Coil setup) + commonMain (hashes already in commons) | |
| `cashu` (10) | Extract | `commonMain` (e.g. `model/nip60Cashu` or `service/cashu`) | parser is pure; split off Compose `GenericLoadable` |
| `call` (10) | Abstract | commonMain (state) + `jvmAndroid` (AudioManager/WebRTC) | WebRTC hard to abstract |
| `location` (6) | Abstract | commonMain (geohash) + `jvmAndroid` (Geocoder) | |
| `connectivity` (3) | Abstract | commonMain (model) + actual | trivial seam |
| `broadcast` (2), `ai` (2), `previews` (2) | Extract | commonMain (`service/`, `preview`) | previews already wraps commons parsers — consolidate |
| `lnurl` (2) | Extract / **dedup** | `jvmAndroid` `service/lnurl` | `LightningAddressResolver` duplicates commons — reconcile |
| `namecoin` (1) | Abstract | `jvmAndroid` | ElectrumX over OkHttp |
| `relays` (1, EOSE) | Extract / dedup | `relayClient`/`relays` | consolidate EOSE state |
| `eventCache` (1) | Abstract | commonMain | eviction logic pure; trim trigger via iface |
| `scheduledposts` (4), `calendar` (4) | Native (model extractable) | — | WorkManager-bound; model → commonMain if needed |
| `crashreports` (4), `logging` (3) | Native (DTO extractable) | — | Choreographer/Build native |
| `tts` (2), `cast` (2), `nests` (2) | Native | — | TTS / Cast / foreground service |
| root utils (`ByteFormatter`, `CountFormatter`, etc.) | Extract | `util/` | some may already exist — check first |
**Highest value, lowest friction:** `okhttp`→`jvmAndroid`, `cashu`, `broadcast`,
`ai`, `previews` consolidation, `lnurl` dedup, EOSE consolidation.
---
## 4. ui/ — migration matrix
commons rule (from `ARCHITECTURE.md`): cross-cutting composables → `ui/<area>`;
feature-specific → `<feature>/ui`. Deciding test: "could an unrelated feature
reuse this as-is?"
| Area (files) | Shareable | Destination | Blockers |
|---|---|---|---|
| `ui/dal` (5) | already re-exports | — | none — delete shims, point at `ui/feeds` |
| `ui/theme` (5) | ~90%, **dup** | merge into `ui/theme` | system-chrome (status/nav bar) stays Android |
| `ui/feeds` (12) | ~95% | `ui/feeds` | a few `R.string`, AccountVM |
| `ui/layouts` (9) | ~85% | `ui/layouts` | 2× `R.string`, 1× `INav` |
| `ui/components` (59) | ~75% | split `ui/components` (atoms) + `<feature>/ui` (galleries) | ~30 atoms clean; Coil/AccountVM/`R.string` on the rest |
| `ui/note` (184) | ~5070% | formatters→`ui/text`; avatars→`ui/elements`/`profile/ui`; renderers→`<nipNN>/ui` | AccountVM (29), `R.string` (24), Note model coupling |
| `ui/actions` (41) | ~65% | transforms→commonMain; VMs→`viewmodels`; media→stay Android | Context (7), AccountVM (7), `R.string` (8) |
| `ui/screen` (971) | mostly native | — | app screens; harvest reusable atoms opportunistically |
| `ui/navigation`, `ui/call`, `ui/cast`, `ui/broadcast`, `ui/tor` | native | — | platform wiring |
**Lowest-friction UI wins** (after string migration): TimeAgo/date formatters →
`ui/text`; `ClickableUrl/Email/Phone`; `GenericLoadable` → `ui/state`; input
transforms; `DisappearingBar*` → `ui/layouts`; feed-state composables; `ui/dal`
shim deletion; `ui/theme` consolidation. ~42% of note/components/actions
composables (112/266) carry no `R.string` at all.
---
## 5. Reconciliation / dedup backlog (do early — prevents drift)
1. `service/relayClient` (Android) vs `commons/relayClient` — keep Android as
wiring only; lift cache-independent EOSE managers down.
2. `service/lnurl/LightningAddressResolver` vs `commons/service/lnurl/…` — one
canonical copy.
3. `ui/note/PubKeyFormatter` vs `commons/util/PubKeyFormatter` — delete the dup.
4. `ui/theme` vs `commons/ui/theme` — single color/type source.
5. `ui/dal` vs `commons/ui/feeds` — delete re-export shims.
6. `service/relays/EOSE*` scattered — one EOSE-state home.
---
## 6. Phased roadmap
- **Phase A — Account keystone (largest, gates the rest).** Extract `Account` +
`LocalCache` business logic into `commons/model/account` + `model/cache`
behind the existing interfaces; amethyst keeps a thin Android adapter
(LocationState, Compose `@Stable`). Unblocks ViewModels + interactive UI.
- **Phase B — pure model & service extracts (parallel, low risk).** The ~16
`nipNN` model packages, `topNavFeeds`, `cashu`, `broadcast`, `ai`,
`previews`, EOSE/lnurl/PubKeyFormatter dedups, `okhttp`→`jvmAndroid`.
- **Phase C — storage/network seams.** `keystorage` expect/actual for prefs +
KeyStore + marmot stores; HTTP packages (nip11/nip03/namecoin/privacyOptions)
→ `jvmAndroid`.
- **Phase D — string migration + shared UI atoms.** Migrate the needed strings
into `composeResources`, then extract the low-friction UI list in §4.
- **Phase E — feature UI.** Note type renderers → `<nipNN>/ui`; read-only
renderers (Badge, Emoji, Poll display) before interactive ones (reactions,
composers). Much of `ui/note/creators` and reaction/zap rows may stay native
unless Desktop/iOS need them.
- **Never moves:** Application/DI, notifications, playback, WorkManager jobs,
TTS, Cast, foreground services, navigation shells, Activity-bound code.
## 7. Suggested first PR (smallest valuable slice)
Phase B subset — zero account dependency, immediately reusable by Desktop:
the dedups (§5.25.5) + a bulk move of 46 pure `nipNN` model packages
(`nip17Dms`, `nip30CustomEmojis`, `nip47WalletConnect`, `nip65RelayList`,
`trustedAssertions`, `zap`). Mechanical, compile-verifiable, no new abstractions.
Phase A is its own large PR and should be planned separately.
---
## 8. Architecture end-state (why the tree below is shaped this way)
The ports already exist in commons (`IAccount`, `ICacheProvider`,
`ICacheEventStream`) but **every app reimplements them** — `amethyst`
(`object LocalCache` 3917 LOC + `class Account` 3565 LOC), `desktopApp`
(`class DesktopLocalCache` 739 LOC + `DesktopIAccount` 278 LOC), `cli`
(stateless). Crucially these implementations are **platform-agnostic already**
(`Account.kt` has one android import — `@Stable` — plus a single `LocationState`
coupling; `LocalCache.kt` has two; `DesktopLocalCache` has zero). So this is
**accidental forking, not essential divergence.** The end-state is **one
canonical implementation per port, living in commons**, with apps keeping only
thin platform adapters.
Two consequences shape the package tree:
1. **Decompose the monoliths; don't relocate them.** `Account` (28 `StateFlow`s +
~170 `suspend fun`s) splits into *state* (per-user reactive state), *actions*
(the verbs), and a thin *façade* implementing `IAccount`. `LocalCache`
becomes the cache *engine* as a `class` (so every platform gets multi-account;
Desktop already needed it).
2. **Litmus test = CLI-constructibility.** `signer → cache → account state →
actions → relay client → pure services` must build in a headless `main()`
with no Compose/Android. Anything that can't is mislayered or missing a port.
Layer is the primary axis; NIP is the secondary axis (`nipNN<slug>` matching
quartz). The raw per-NIP list **state** lives in `model/nipNN<slug>`; the
account's *composed/derived* view of those lists lives in `model/account/*`. The
generic `state/` package (load-a-thing `StateFlow` machines) is **not** where
account-specific state goes.
## 9. Target package hierarchy (the destination tree)
`commonMain/.../commons/` (✚ = new package, ⤺ = merge into existing, ✔ = exists):
```
model/
├─ cache/
│ ├─ ICacheProvider.kt ICacheEventStream.kt ✔ read ports
│ ├─ ILocalCache.kt ✚ write port (from amethyst)
│ ├─ LocalCache.kt ← unified cache ENGINE as a CLASS (amethyst object + DesktopLocalCache collapse here)
│ └─ UserMetadataCache.kt ✔
├─ account/
│ ├─ Account.kt ← façade implementing IAccount (from amethyst Account)
│ ├─ AccountInfo.kt AccountStorage.kt SignerType.kt ✔
│ ├─ settings/ AccountSettings, AccountSyncedSettings(+Internal) ✚
│ ├─ follows/ derived per-feed live-follow lists + all-follows merge ✚
│ ├─ relays/ account relay sets + outbox relay cache ✚
│ └─ hidden/ LiveHiddenUsers — mute/spam aggregated view ✚
├─ nip02FollowList/ ⤺ raw Kind-3 follow-list state (amethyst nip02FollowLists)
├─ nip65RelayList/ ⤺ relay-list state
├─ nip51Lists/ ⤺ mute / bookmark / people lists
├─ nip30CustomEmojis/ ⤺ emoji-pack state
├─ nip01Core/ ⤺ kind-0 user-metadata relay-hint state (amethyst nip01UserMetadata)
├─ nip17Dm/ nip47WalletConnect/ nip60Cashu/ nip72ModCommunities/
│ nip78AppData/ nip85TrustedAssertions/ nip86RelayManagement/
│ nipA3PaymentTargets/ nipB7Blossom/ nipBCOnchainZaps/ ✚ per-NIP state holders (bulk)
└─ settings/ ✚ UiSettings / UiSettingsFlow (global, NOT account-scoped)
state/ ✔ generic StateFlow machines (FollowState, …) — unchanged
actions/ ✔ the account "verbs" — extend, don't duplicate
│ existing: DmActions FollowActions SearchActions ZapActions ZapSplitResolver
│ add: PostActions ReactionActions BookmarkActions MuteActions
│ RelayActions ProfileActions GroupActions DeletionActions …
feeds/
├─ custom/ ✔
├─ topNav/ ✚ (amethyst model/topNavFeeds)
└─ algo/ ✚ (amethyst model/algoFeeds)
relayClient/ ✔ + cache-free EOSE managers lifted from amethyst service/relays
keystorage/
├─ SecureKeyStorage.kt ✔
├─ preferences/ ✚ LocalPreferences logic + DataStore/SharedPrefs schema (+actuals)
├─ account/ ✚ account persistence (pairs with model/account/AccountStorage)
└─ marmot/ ✚ MLS key-package / message / group-state store ports (+actuals)
service/
├─ upload/ nwc/ lnurl/ ✔ (lnurl: dedup amethyst LightningAddressResolver)
├─ broadcast/ ai/ connectivity/ eventCache/ ✚ commonMain (pure)
└─ okhttp/ nip11RelayInfo/ nip03Timestamp/ namecoin/ privacyOptions/ ✚ jvmAndroid (OkHttp/Jackson)
tor/ ✔ + torState relay-evaluation logic
```
UI destinations follow `commons/ARCHITECTURE.md` (cross-cutting → `ui/<area>`,
feature-specific → `<feature>/ui`) and are detailed in §4 — out of scope for the
"common objects" core tier above.
## 10. Package-naming decisions (the rules behind §9)
1. **`model/nipNN<slug>` (raw list state) vs `model/account/*` (composed view).**
`model/nip02FollowList` holds the raw Kind-3 list; `model/account/follows`
holds the user's *derived* per-feed follow flows that compose it. Same for
relays (`nip65RelayList` vs `account/relays`) and mutes (`nip51Lists` vs
`account/hidden`).
2. **NIP slugs match quartz exactly**, normalizing amethyst's drift:
`nip02FollowLists→nip02FollowList`, `nip01UserMetadata→nip01Core`,
`nip30CustomEmojis` (keep — existing commons name), `nip72Communities→
nip72ModCommunities`, `nip78AppSpecific→nip78AppData`,
`trustedAssertions→nip85TrustedAssertions` (or keep the existing readable
`model/trustedAssertions` — pick one and converge).
3. **`state/` stays generic.** Account-specific state never goes here; it goes in
`model/account/*`. `state/` is reusable load-a-thing `StateFlow` machines.
4. **`actions/` = verbs, `model/` = nouns/state, `cache/` = the store.** The 170
`Account` methods become `*Actions` files grouped by concern, taking
`signer + cache + relayClient`.
5. **`keystorage/` = persistence ports + actuals; the settings *schema* is
`model/account/settings`.** Schema (what) and persistence (how/where) are
separate packages.
6. **`service/` name is source-set-agnostic.** Pure services sit in `commonMain`,
OkHttp/Jackson ones in `jvmAndroid` — same `service/<x>` path, different
source set (per `ARCHITECTURE.md` §"Source sets").
7. **Keep the name `LocalCache`** for the engine class (222 amethyst files / 560
call-sites reference it); amethyst keeps a thin `object LocalCache` delegating
to one shared `class` instance during transition.