Files
amethyst/commons/plans/2026-09-12-commons-ui-split.md
T
Claude 97b13b7c47 refactor: move reply-context logic to commons.model; keep the Compose compiler in commons by measurement
Closes the last two items of documented debt from the commons/commonsUI split.

ParentNote (replyingDirectlyTo, isCommunityDefinition) and ReplyContext are
pure thread logic used by ViewModels, so they move from the misleading
`ui.note` package to `commons.model`, next to ThreadAssembler, together with
their tests and the StubCache fixture that shared the package. No `ui.*`
package is left in commons. The `ui.note` composables in commonsUI gain
explicit imports; consumer imports rewritten.

Whether commons still needs the Compose compiler plugin was an open question;
it is now measured. With compiler reports on the three GUI modules and full,
non-incremental recompiles in both configurations, removing the plugin flips
composable parameters typed with unannotated commons classes (TopFilter,
TorSettings, ProfileBroadcastStatus, ScheduledPost, EmojiPackState, ...) from
runtime-stable to unstable: 20→28 in commonsUI, 33→65 in desktopApp,
90→149 in amethyst. The plugin stays; the numbers are recorded in the build
file, ARCHITECTURE.md and the split plan so the question is not reopened.

Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug
compiles for nappletHost and amethyst; commons/commonsUI/cli/desktopApp JVM
test suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 20:58:28 +00:00

113 lines
5.5 KiB
Markdown

# Split `commons` into `commons` (headless) + `commonsUI` (Compose)
**Date:** 2026-09-12
**Status:** shipped on this branch
## Why
`cli` (`amy`) depends on `:commons`. Until now `:commons` declared Compose
UI, Coil, Compose resources, markdown and (on JVM) `compose.desktop.currentOs`
as dependencies, so every CLI distribution dragged ~40 MB of Compose + Skiko
jars it never loads (`create-release.yml` had a 200 MB budget "until commons
is split into core + ui modules"; `BUILDING.md` flagged the same for the
Homebrew bundle). The UI / non-UI boundary documented in
`commons/ARCHITECTURE.md` §1 was a convention enforced by nothing.
## What
A new KMP module **`:commonsUI`** (same targets and source-set layout as
`:commons`, plus `skikoMain`) that `api`-depends on `:commons` and owns every
Compose-dependent file. `:commons` keeps only the Compose *runtime*
(`@Stable`/`@Immutable`, snapshot state) and `lifecycle-viewmodel`; it no
longer applies the `org.jetbrains.compose` plugin, has no `composeResources`,
no Coil, no markdown, no `highlights`, no desktop Compose.
**Files keep their packages.** The split is a build-graph boundary, not a
package rename: `231` files were `git mv`'d from `commons/src/…` to
`commonsUI/src/…` and not a single import changed in `amethyst`,
`desktopApp`, `nappletHost` or `cli`. The generated `Res` class stays at
`com.vitorpamplona.amethyst.commons.resources` for the same reason.
Consumers: `amethyst`, `desktopApp`, `nappletHost` (for
`NappletWebContract`, which reads the shell/shim from `composeResources`) and
`benchmark` (robohash) gained `project(":commonsUI")`. `cli`, `geode`,
`marmotBench` are untouched and must stay that way.
## Method (reproducible)
1. Classify every `.kt` under `commons/src` as **UI** if it imports
`androidx.compose.{ui,foundation,material3,animation}`,
`org.jetbrains.compose.*`, `coil3`, `org.jetbrains.skia`,
`androidx.lifecycle.compose`, `…commons.resources`, or declares
`@Composable`.
2. Build the intra-module reference graph (imports + same-package simple-name
hits) and verify **no headless file references a UI file**. Two real hits
surfaced and were fixed by surgery rather than by moving logic into the UI
module:
- `richtext/GalleryParser` carried a vestigial
`@OptIn(ExperimentalLayoutApi::class)` — dropped; it is pure logic.
- `privacylock/PrivacyLockState` declared the `LocalPrivacyLockState`
CompositionLocal + `lockStateFor()` next to the state machine — the two
Compose members moved to `commonsUI/…/privacylock/LocalPrivacyLockState.kt`
(same package).
3. Additionally move the rest of the UI-only packages (`ui/**`, `icons`,
`hashtags`, `robohash`, `<feature>/ui`) **unless a headless file depends
on the file**. That rule keeps the feed DAL (`ui/feeds/FeedFilter`,
`AdditiveFeedFilter`, `ChangesFlowFilter`, `FeedContentState`,
`RepostRenderability`, `AdditiveComplexFeedFilter`…) and
`ui/note/ParentNote`+`ReplyContext` in `commons` (ViewModels use them).
4. Tests follow their subject; test fixtures shared with headless tests
(`ui/note/StubCache`) stay in `commons`.
5. Check `expect`/`actual` pairs never straddle the boundary (none did) and
that no `internal` declaration is used across it (none was).
## Verified
- `:commons:compileKotlinJvm`, `:commonsUI:compileKotlinJvm`,
`:cli:compileKotlin`, `:desktopApp:compileKotlin`,
`:nappletHost:compileDebugKotlin`, `:amethyst:compileFdroidDebugKotlin`.
- `:commons:jvmTest`, `:commonsUI:jvmTest`, `:cli:test`,
`:commons:verifyKmpPurity`, `:commonsUI:verifyKmpPurity`.
- `:cli` runtime classpath no longer contains `org.jetbrains.compose.ui`,
`foundation`, `material3`, `skiko` or `coil`.
iOS targets could not be linked in the Linux CI container; the workflow runs
`:commonsUI:iosSimulatorArm64Test` + `:commonsUI:compileTestKotlinIosArm64`
next to the `:commons` ones on macOS.
## Follow-ups (done in the same branch)
- **CLI size budget tightened to 120 MB** in `create-release.yml`. Measured
after the split (1.15.2, Linux x64): JVM tarball 55 MB, jlink image tarball
80 MB, `lib/` 60 MB on disk — vs ~70 MB JVM tarball before.
- **Feed DAL moved out of `ui.feeds`** into `commons/…/feeds/` (root of the
existing `feeds` package, next to `feeds/custom`). Consumer imports rewritten.
- **Chess composables moved to `nip64Chess/ui`** in `commonsUI`; the logic
stays in `commons/…/nip64Chess/`.
- **`ui/note/ParentNote` + `ReplyContext` moved to `commons/…/model/`** next to
`ThreadAssembler` (with the `StubCache` test fixture). No `ui.*` package is
left in `commons`.
## Decided: `commons` keeps the Compose compiler plugin
Measured rather than guessed. With `composeCompiler { reportsDestination }`
on the three GUI modules and full (non-incremental, `--rerun`) compiles in
both configurations, removing the plugin from `commons` flips composable
parameters typed with unannotated commons classes from runtime-stable to
unstable:
| Module | unstable params, plugin on | plugin off |
|---|---|---|
| `commonsUI` | 20 | 28 |
| `desktopApp` | 33 | 65 |
| `amethyst` (fdroidDebug) | 90 | 149 |
The classes involved (`TopFilter`, `TorSettings`, `TorServiceStatus`,
`ProfileBroadcastStatus`, `ScheduledPost`, `EmojiPackState`,
`Nip65RelayListState`, `PendingAuthApproval`, `UserSearchEngine`, …) are
all-`val` classes with no `@Stable`/`@Immutable` annotation; the plugin infers
their stability and keeps inferring it as they evolve. Hand-annotating them
would reproduce today's result but rot silently the first time a `var` is
added, so the plugin stays. Recorded in `commons/build.gradle.kts`.