Files
amethyst/commons/ARCHITECTURE.md
T
Claude f94fbe25b1 refactor: put the moved UI in the documented commons layout and fix the audit findings
A review of this branch against commons/ARCHITECTURE.md found the first
moves had kept the app's layer-first paths (commons.ui.screen.loggedIn.<x>)
and put headless code under ui.*. Everything now follows the documented
taxonomy:

  - Feature UI lives in <feature>/ui, named after its quartz package:
    nip51Lists/ui (bookmark groups, people lists, follow packs, interest
    sets), nip52Calendar/ui, nip34Git/ui, nip64Chess/ui, nip23LongContent/ui,
    nip28PublicChat/ui, nip29RelayGroups/ui, nip17Dm/ui, nip56Reports/ui,
    nip72ModCommunities/ui, nipC0CodeSnippets/ui, nipCCGeocaching/ui,
    nipACWebRtcCalls/ui, and concern names where quartz has no NIP (chats,
    marmot, concord, ephemChat, buzz, birdstar, music, cashu, onchain,
    browser, napplet, profile, mediaServers, relays, qrcode, scheduledposts,
    account/ui/login|signup). commons.ui.screen is gone.
  - Nothing under ui.* remains in the commons module. The Route catalog and
    BookmarkType go to model/navigation and model/nip51Lists; the headless
    composer state (AudienceSelection, SplitBuilder, IZapRaiser) to
    model/composer; classifyScannedPayload to qrcode; the scheduled-post
    parsers split out of ScheduledPostMedia into commons scheduledposts,
    leaving only MediaThumbnail in commonsUI. The CLI can now reach them.
  - ARCHITECTURE.md documents ui/feeds and ui/navigation as cross-cutting
    areas and lists the new feature packages.

Duplicates removed:
  - AddButton/RemoveButton existed twice in commonsUI; the unused
    ui/components pair (hard-coded English) is replaced by the localized
    one, which now lives in ui/components/ActionButtons.kt.
  - desktop's own UserDisplayNameLayout was an unused copy of the shared
    one; deleted.
  - Colors.kt merged into Color.kt: empty section headers dropped,
    LightPurple (= Purple200, unused) removed, DefaultPrimary (= Primary80)
    replaced at its one desktop call site.

isLight:
  - The fallback for non-Amethyst backgrounds (desktop's palette, audio
    rooms whose theme overrides the background) is now memoized for the
    last background seen, so those screens pay for luminance() once per
    background instead of on every themed-color read. The comment said
    "single reference comparison", which was no longer true; rewritten.
  - IsLightTest covers both Amethyst schemes across accents, desktop's
    #121212 / #F2F2F7 ramps, themed-room backgrounds and the memo.
  - Behaviour note: NestThemedScope rooms with a dark background used to
    count as light (anything not pure black did), so their gray text,
    placeholders and borders took the light-theme values. They now follow
    the room background's luminance.

Also:
  - Baseline profile: the entries for every class this branch moved
    (theme facades, the Theme.kt members that became AmethystColorScheme /
    MarkdownStyle, routes, the moved composables) are rewritten to the new
    class names so ART still AOT-compiles them. The profile has older stale
    entries from earlier migrations (datasource assemblers, LocalCache,
    okhttp) that need a real regeneration on a device.
  - Two inline fully-qualified Route references in the nests lobby use the
    import instead.
  - GitStatusPill's KDoc said the index read "stays native"; it no longer
    does.
  - The android-expert skill and its navigation reference point to the new
    Route/INav/theme locations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168wY9t7i9NC5u3svyMxEz6
2026-09-23 19:33:50 +00:00

286 lines
21 KiB
Markdown

# `commons` — Shared Module Architecture & Goals
`commons` is the **shared layer** between every Amethyst front end:
| Consumer | Kind | Uses from `commons` |
|----------------|------------------------------|----------------------------------------------|
| `amethyst` | Android app (touch-first) | everything (models, state, ViewModels) + `commonsUI` |
| `desktopApp` | Desktop JVM app (mouse-first)| everything (models, state, ViewModels) + `commonsUI` |
| `cli` (`amy`) | Headless JVM CLI (no UI) | everything — `commons` is headless by construction; it never sees `commonsUI` |
| `nappletHost` | Android WebView sandbox | napplet contract + `commonsUI` (for the shell/shim Compose resources) |
| iOS (future) | iOS app | everything + `commonsUI`; expected to share most UI with Android |
`commons` sits **above** `quartz` (the protocol-only Nostr KMP library) and
**below** the apps. The split between the three is:
- **`quartz/`** — Nostr protocol: events, NIPs, crypto, relay framing. No app
state, no UI, no caches of "what this user follows."
- **`commons/`** — everything an Amethyst *client* needs that isn't a
platform-native screen, navigation shell, **or Compose UI**: domain models
(`Note`, `User`), in-memory state holders, ViewModels, the relay-subscription
client, shared business services.
- **`commonsUI/`** — the Compose UI components that more than one front end
renders, plus everything only they need (icons, theme, Coil fetchers,
markdown, the `composeResources` strings/fonts and the generated `Res`).
Depends on `commons` as `api`. See `commonsUI/ARCHITECTURE.md`.
- **`amethyst/` & `desktopApp/`** — platform-native screens, navigation
(bottom-nav vs sidebar), gestures, system integration. They assemble
`commons` pieces; they should not re-implement them.
> The goal is **"write it once in `commons`"**. Before adding a manager, cache,
> filter, ViewModel, or composable to an app module, check whether it already
> exists here or belongs here. See the "Where does my code go?" guide below.
---
## 1. The one rule that shapes the package tree: the **UI / non-UI boundary**
The shared layer is **two modules** with one package tree:
> **`commons` = CLI-safe code.** It does not depend on Compose UI. It may use
> the `androidx.compose.runtime` *annotations* `@Stable` / `@Immutable` (they
> are just stability tags) and snapshot state (`mutableStateOf`, `State`), but
> it must **not** import `androidx.compose.ui`, `androidx.compose.foundation`,
> `androidx.compose.material3`, Coil, the generated `Res`, declare
> `@Composable` functions, or build `ImageVector`s. Its `build.gradle.kts`
> simply has none of those dependencies, so a violation fails to compile.
>
> **`commonsUI` = UI code.** Anything that does the above. It is only usable
> by the GUI front ends (Android, Desktop, iOS), never by `cli`.
Both modules share the **same `com.vitorpamplona.amethyst.commons.*` package
tree** — the split is a module boundary, not a package rename, so a file moves
between `commons/src/…` and `commonsUI/src/…` without changing its package or
any consumer's imports. Kotlin resolves same-package declarations across
modules without imports; the only thing that stops working across the boundary
is `internal` visibility (a UI file cannot see an `internal` declaration in
`commons` — make it public or move it).
This boundary is **not** a top-level `ui/` vs `logic/` partition of the package
tree (we chose to stay feature-oriented, §3). It is a property of each file:
a feature keeps its logic in `commons/…/<feature>/` and its composables in
`commonsUI/…/<feature>/ui/` (or `commonsUI/…/<feature>/` for the historical
flat packages), and the table in §2 says which module each package lives in.
---
## 2. Package taxonomy
Top-level packages under
`commonMain/.../commons/`, grouped by concern. **UI?** marks whether the
package contains Compose UI (and therefore lives in **`commonsUI`**, not
here). "mixed" means the feature's logic is in `commons` and its composables
in `commonsUI`, under the same package.
### Domain models & data
| Package | UI? | Purpose |
|----------------|-----|---------|
| `model` | no¹ | Core domain types (`Note`, `User`, `Channel`), thread assembly (`ThreadAssembler`, `ThreadLevelCalculator`, `ReplyContext`, `replyingDirectlyTo`), and per-NIP event model extensions in `model/nipNN…` subpackages. `model/cache` holds the in-memory event store: the `ICacheProvider` / `ILocalCache` ports and `UserMetadataCache` in commonMain, and the concrete `LocalCache` (plus `AntiSpamFilter`, `CachePruner`, `CacheSearch` and the `LocalCacheHost` app-shell port) in jvmAndroid. `model/account`, `model/observables`. The largest package; keep it organized by NIP. |
| `defaults` | no | Static bootstrap data (default relays, channels). |
`model/navigation` also holds the headless navigation identifiers: `NavBarItem`, `BottomBarEntry` and the app's `@Serializable` `Route` catalog (`Routes.kt`). `model/composer` holds post-composer state that is not UI (`AudienceSelection`, `SplitBuilder`, `IZapRaiser`).
¹ `model` uses only the `@Stable`/`@Immutable` runtime annotations — CLI-safe.
### Protocol-adjacent business logic (CLI-safe)
| Package | UI? | Purpose |
|----------------|-----|---------|
| `actions` | no | Event builders for user actions (follow, zap…). The canonical entry point for non-UI callers. |
| `account` | mixed | New-account bootstrap events; the logged-off login/sign-up buttons in `commonsUI` `account/ui/login` and `account/ui/signup`. |
| `onchain` | mixed | On-chain zap splitting/broadcasting; user-facing failure strings in `commonsUI` `onchain/ui`. |
| `marmot` | mixed | MLS group-chat event processing; group-chat composables (retention picker, agent stream banner) in `commonsUI` `marmot/ui`. |
| `nip53LiveActivities` | mixed | Live-activity zapper aggregation (logic) + the stream card in `nip53LiveActivities/ui`. |
| `search` | no | Event search filtering/ranking, kind registry. |
| `preview` | no | OpenGraph / meta-tag link-preview parsing. |
| `emojicoder` | no | Variation-selector emoji encode/decode. |
| `richtext` | no | URL/media/pattern parsing for rich text. |
| `blurhash` | no | BlurHash encode/decode (pure math; platform image bridge in platform sets). |
| `thumbhash` | no | ThumbHash encode/decode. |
| `nipACWebRtcCalls` | mixed | NIP-AC WebRTC **call state machine** + peer-session abstraction. See `nipACWebRtcCalls/ARCHITECTURE.md`. (Mirrors `quartz/.../nipACWebRtcCalls`.) Call-screen previews in `commonsUI` `nipACWebRtcCalls/ui`. |
| `qrcode` | mixed | Scanned-payload classification (`classifyScannedPayload`: NIP-19, `nostr:` URIs, relays, NWC/LN) here; scanner sheets in `commonsUI` `qrcode/ui`. |
| `scheduledposts` | mixed | Scheduled-post model + signed-event parsers (`extractFirstMediaUrl`, `extractEventId`, `extractContentPreview`); the `MediaThumbnail` composable in `commonsUI` `scheduledposts/ui`. |
### State holders & ViewModels
| Package | UI? | Purpose |
|----------------|-----|---------|
| `state` | no² | Small feature `StateFlow` machines (`FollowState`, `UserMetadataState`, `LoadingState`). |
| `viewmodels` | no² | Larger list/feed-backed ViewModels (`androidx.lifecycle.ViewModel`). Shared by all GUI front ends; `cli` usually drives the layers below instead. The few that hold Compose UI state (`ChatNewMessageState` — `TextFieldValue`; `thread/LevelFeedViewModel` — `LazyListState`) live in `commonsUI` under the same package. |
| `feeds` | no | The feed data-access layer at the root (`FeedFilter`, `AdditiveFeedFilter`, `AdditiveComplexFeedFilter`, `ChangesFlowFilter`, `FeedContentState`, `FeedState`, `InvalidatableContent`, `DefaultFeedOrder`, `RepostRenderability`…) plus `feeds/custom` (`FeedDefinitionRepository` — custom-feed definitions & ordering) and `feeds/related`. See the `feed-patterns` skill. |
| `profile` | mixed | `ProfileBroadcastStatus` (state) + `EditProfileFields` at the root; the `ProfileBroadcastBanner` composable lives in `commonsUI` `profile/ui`. |
| `privacylock` | mixed | Lock state machine + settings here; `LocalPrivacyLockState`/`lockStateFor` (CompositionLocal accessor) in `commonsUI`. |
² may touch `compose.runtime` state types (snapshot state, `@Stable`); they
are shared across the GUI apps. A state holder that needs a `foundation`/`ui`
type (`LazyListState`, `TextFieldValue`, `TextFieldState`) goes to `commonsUI`.
### Relay client
| Package | UI? | Purpose |
|----------------|-----|---------|
| `relayClient` | mixed | Compose-scoped subscription managers, filter assemblers, EOSE managers, preloaders. (Despite a `composeSubscriptionManagers` subpackage name, this is subscription-lifecycle logic, not UI.) The `@Composable` entry points — `relayClient/user/` (`observeUser*` — kind-0 metadata), `relayClient/event/` (`EventFinderFilterAssemblerSubscription`/`observeNote*`), the other `*FilterAssemblerSubscription`s, `KeyDataSourceSubscription`, `auth/AuthApprovalBanner` — are in `commonsUI` under the same packages. See the `relay-client` skill. |
| `relays` | mixed | Low-level EOSE/relay-timing bookkeeping (`EOSECache`, `EOSERelayList`); relay-list settings composables (`DraggableRelayList`, `RelayEventCountRow`, the NIP-45 count result types) in `commonsUI` `relays/ui`. |
### Platform abstractions (`expect`/`actual`)
| Package | UI? | Purpose |
|----------------|-----|---------|
| `util` | no | KMP primitives: `KmpLock`, `WeakReference`, number/URL/codepoint helpers, list/debug helpers. **This is the only general-utility package** — there is no `utils`. |
| `keystorage` | no | Secure key storage interface (Keystore / keychain / keyring actuals). |
| `tor` | no | Tor manager interface + settings. |
| `service` | no | Cross-cutting services: `BundledUpdate` batching (common); `service/upload` (JVM), `service/nwc`, `service/lnurl` (jvmAndroid). **Singular `service`** — there is no `services`. |
### UI (Compose — lives in **`commonsUI`**)
| Package | UI? | Purpose |
|----------------|-----|---------|
| `ui` | yes | **Cross-cutting** shared composables only, organized by area: `ui/components`, `ui/theme`, `ui/signing`, `ui/thread`, `ui/note`, `ui/richtext`, `ui/search`, `ui/notifications`, `ui/screens`, `ui/layouts`, `ui/feeds` (feed shell: empty/error/loading states, refresh box, remembered scroll states), `ui/navigation` (`INav`, `EmptyNav`, top bars, drawer swipe), `ui/markdown`, `ui/privacylock`, plus Compose helpers in `ui/state` (cached-state) and `ui/text` (TextField extensions). Feature-specific UI lives in `<feature>/ui`, **not** here. Nothing under `ui.*` lives in *this* module any more: the feed DAL that used to sit in `ui/feeds` is now `feeds/`, and the reply-context logic that sat in `ui/note` (`replyingDirectlyTo`, `ReplyContext`) is now in `model/` next to `ThreadAssembler`. |
| `nip23LongContent` | yes | Long-form (NIP-23) article UI: `nip23LongContent/ui/article` (reader) + `…/ui/editor` (authoring). The model lives in `model/nip23LongContent` (here). |
| `icons` | yes | `ImageVector` icon definitions + builders, Material Symbols codepoints, the icon-font glyph tables. |
| `hashtags` | yes | Custom hashtag `ImageVector`s. |
| `robohash` | yes | Procedural robohash avatar `ImageVector` assembly. |
| `audio` | mixed | Spectrum/visualizer *data* (`AudioSpectrum`, `SpectrumAnalyzer`…) here; the `VisualizerRenderer`s, `VisualizerRegistry` and the canvas composables in `commonsUI`. |
| `service/image` | mixed | `CoilImageBridge` + the BlurHash/ThumbHash/Base64/Blossom Coil fetchers are `commonsUI` (they are Coil); the headless image helpers stay here. |
| `napplet` | mixed | Protocol/permission logic here; `NappletWebContract` (serves the shell/shim from `composeResources`) in `commonsUI`. |
| `favorites`, `nip30CustomEmojis`, `nip34Git`, `nip85TrustedAssertions`, `nip53LiveActivities` | mixed | Logic here; each feature's `ui/` (or the flat `FavoriteAppIcon`, `EmojiSuggestionState`) in `commonsUI`. |
| `chats` | yes | Composables shared by every chat kind (DMs, public chats, relay groups, concord): unread badge, divisors, system messages, author line, send button, reply toggle, new-conversation screen. Chat *models* are in `model/chats`. |
| `nip17Dm`, `nip23LongContent`, `nip28PublicChat`, `nip29RelayGroups`, `nip51Lists`, `nip52Calendar`, `nip56Reports`, `nip72ModCommunities`, `nipC0CodeSnippets`, `nipCCGeocaching`, `birdstar`, `buzz`, `cashu`, `concord`, `ephemChat`, `music`, `mediaServers`, `browser`, `profile`, `napplet` | yes / mixed | Single-feature UI under `<feature>/ui` (named after the `quartz` package, or its concern name when `quartz` has none). Where the package also has logic (`cashu`, `browser`, `napplet`, `profile`) that part stays here; the `buzz` and `concord` models are in `model/buzz` and `model/concord`. |
### Mixed (documented debt — see §4)
| Package | UI? | Purpose |
|----------------|-----|---------|
| `nip64Chess` | mixed | Live-chess feature: game/lobby/subscription logic here; board/lobby composables in `commonsUI` under `nip64Chess/ui`. (Mirrors `quartz/.../nip64Chess`.) |
| `domain` | no | Currently only `domain/nip46` (Nostr Connect signer flows). Sparse; candidate to fold into a clearer home. |
---
## 3. Conventions
### Feature-oriented, not layer-partitioned
We keep a feature's model, state, and UI **together** under one feature
package rather than splitting the whole module into top-level `ui/` /
`viewmodels/` / `model/` layers. Within a feature, separate UI from logic with a
`ui` **subpackage** (e.g. `profile/ProfileBroadcastStatus` vs
`profile/ui/ProfileBroadcastBanner`) so the CLI-safe boundary (§1) stays
visible.
The big shared cross-feature packages (`model`, `ui`, `relayClient`, `util`,
`icons`) are the exception — they are organized by layer because many features
share them.
### Naming
- **Singular, no synonyms.** `util` (not `utils`), `service` (not `services`).
One concept → one package name.
- **Feature UI vs cross-cutting UI — the deciding test.** A composable goes in
`<feature>/ui` if it renders/edits *one* feature's content (it would make no
sense outside that feature) — e.g. `profile/ui`, `nip53LiveActivities/ui`,
`nip23LongContent/ui`. It goes in `ui/<area>` only if it is reusable across
features (theme, avatars, buttons, layouts, markdown rendering, shimmer…).
When in doubt, ask "could a second, unrelated feature reuse this as-is?" —
yes → `ui/<area>`, no → `<feature>/ui`. The top-level `ui/` package holds
**no** feature-specific composables.
### NIP as the second axis (mirror `quartz`)
`quartz` is ~94% organized by NIP (`nipNN<slug>` per spec), and that is correct
*there* — the protocol layer is naturally NIP-partitioned. `commons` is **not**
organized by NIP at the top level, and should not be: most of it is
cross-cutting infrastructure (`ui`, `relayClient`, `viewmodels`, `feeds`,
`util`…) that serves many NIPs at once, and the load-bearing UI/non-UI boundary
(§1) cuts *across* NIPs, so a NIP-first top level would just nest the same
problem one level down.
Instead, **layer is the primary axis, NIP is the secondary axis**:
- The big shared layers stay layer-organized (`model`, `relayClient`, the
cross-cutting `ui`, …).
- **Inside a layer, NIP-specific code goes in a `nipNN<slug>` subpackage whose
name matches `quartz` exactly** — e.g. `model/nip57Zaps`. This gives a clean
trace: `quartz/nip57Zaps` → `commons/model/nip57Zaps`.
- **A top-level package that *is* a single self-contained NIP feature takes the
same name as its `quartz` counterpart**, and owns its own UI under
`<feature>/ui`: `nip64Chess`, `nipACWebRtcCalls`, `nip53LiveActivities`,
`nip23LongContent`, `marmot`. (`marmot` is un-numbered in `quartz` too.)
Generic/multi-NIP packages keep their concern name (`search`, `preview`,
`actions`, `richtext`…).
- **Feature UI is never under `ui/`.** A single-NIP feature's composables live
in `<feature>/ui` (e.g. `nip53LiveActivities/ui`), not `ui/nip53LiveActivities`.
`ui/` is exclusively cross-cutting (§2, Naming).
### Source sets
| Source set | For |
|---------------|-----|
| `commonMain` | KMP code for **all** targets (Android, JVM, iOS). Gated by `verifyKmpPurity` — no Jackson/OkHttp/`System.currentTimeMillis`/`java.util.UUID`/JVM `@Synchronized`/`@Volatile`. Use the KMP replacements. |
| `jvmAndroid` | Shared by Android + Desktop, **not** iOS. Where JVM-bound deps live (`nestsClient`, OkHttp, NWC/LNURL). |
| `jvmMain` | Desktop-only (keyring, EXIF, `service/upload`, OS notifications). `dependsOn(jvmAndroid)`. |
| `androidMain` | Android-only (Keystore, DataStore, the Android `R` string resources used by the napplet host). `dependsOn(jvmAndroid)`. |
| `iosMain` | iOS `actual`s. Compile-only spike today. |
`commonsUI` mirrors the same source-set layout (plus `skikoMain`, shared by
desktop JVM + iOS for `org.jetbrains.skia` pixel helpers); Coil-OkHttp,
markdown and the `viewModel()` helper live in its `jvmAndroid`.
When adding platform code, prefer the **most common** source set that still
compiles: `commonMain` → `jvmAndroid` → platform-specific. See
`/kotlin-multiplatform`.
### Where does my code go? (quick guide)
1. **Pure Nostr protocol** (events/NIPs/crypto)? → not here, it's `quartz`.
2. **A composable** rendered by ≥2 front ends, or that you want iOS to share? →
`commonsUI`, in `ui/<area>` or `<feature>/ui` (same package tree as here).
Also anything that imports Coil, `Res`, or a `foundation`/`ui` state type.
3. **A ViewModel / `StateFlow` state holder**? → `viewmodels` or `state` (or
`<feature>` if feature-scoped). Keep it CLI-safe where practical.
4. **Relay subscription / filter assembly**? → `relayClient`.
5. **A domain model or per-NIP event wrapper**? → `model` (`model/nipNN…`).
6. **A platform capability behind `expect`/`actual`** (storage, crypto)? → the matching abstraction package + actuals in platform sets.
7. **A generic helper**? → `util`. (Resist creating a new top-level package for
one file.)
---
## 4. Known debt / follow-ups
These are intentionally *documented*, not silently tolerated. Fix opportunistically.
- **`commons` applies the Compose *compiler* plugin without declaring any
composable.** Deliberate, not debt: the plugin's `@StabilityInferred`
stamps are what keep unannotated commons classes stable from the apps'
point of view. Measured on full recompiles (2026-09-12): without it,
unstable composable params go 20→28 in `commonsUI`, 33→65 in `desktopApp`,
90→149 in `amethyst`. Don't remove it; if a class must be stable for a
hot path, annotate it explicitly as well.
- **Same package tree in two modules.** Intentional (zero-import-churn split),
but it means a package's module is not visible from its name. Rule of
thumb: if it imports Compose UI it is in `commonsUI`; check §2 when unsure.
- **`domain` is sparse** (only `nip46`). Either grow it as the home for
use-case/flow types or rename it to the matching `nip46RemoteSigner` per the
NIP-second-axis rule.
- **`relays` vs `relayClient`** are coherent but close in name; `relays` is
low-level EOSE bookkeeping, `relayClient` is the subscription client. Keep the
distinction in mind when adding files.
- Several **single-file feature packages** (`account`, `marmot`,
`nip53LiveActivities`, `keystorage`) are kept as feature/abstraction
namespaces expected to grow; do not fold them into `util` just for size.
- **`onchain`** (on-chain zap splitting) is `quartz`-adjacent but un-numbered;
leave readable unless a clear NIP number lands.
- **`model/cache/EventCache` is `jvmAndroid`, not `commonMain`.** The NIP-95
`java.io.File` spill is the obvious blocker but not the binding one: the
cache's own storage, `LargeSoftCache`, is `jvmAndroid`
(`WeakReference` + `ConcurrentSkipListMap`), as are the two
`*ListMatchingFilter` observables, `MintDirectoryIndex` and
`NwcPaymentTracker`. Promoting the cache means promoting those first.
The binding constraint is `LargeSoftCache`'s **sorted** store: it backs the
ranged `forEach(from, to, …)` that all of `LargeSoftCacheAddressExt` uses to
scan one kind's slice of the `Address` key space, and a hash map turns each
of those into a full scan. `quartz/linuxTest/LargeCacheRangeFallbackTest`
documents the matching invariant from the other side — the native range
overloads fall back to full scans, and that is deemed safe precisely
*because* the range callers live in the JVM-only `LargeSoftCache`. Moving
this needs a sorted KMP store first, not just the okio blob sink and the
`java.util.SortedSet` signature change. See the audit in
`commons/plans/2026-08-30-commons-migration-sweep.md`.
---
## 5. See also
- `nipACWebRtcCalls/ARCHITECTURE.md` — WebRTC call state machine deep-dive.
- Root `.claude/CLAUDE.md` — module overview, sharing philosophy, build commands.
- `/kotlin-multiplatform`, `/compose-expert`, `/feed-patterns`, `/relay-client`,
`/account-state` skills for the patterns referenced above.