Sleevesight

Sleevesight considerations

Date: 2026-10-12
Belongs to: architecture.md, decisions D2 and D3

The reasoning behind architecture choices that need more room than a row in the decisions table.

D2. Plain Kotlin or Kotlin Multiplatform for the pipeline

“Plain Kotlin” means ordinary Kotlin libraries compiled for the JVM. Android apps run JVM bytecode, so the command-line tool and the app share them directly.

Plain Kotlin

Pros:

Cons:

Kotlin Multiplatform

Pros:

Cons:

Recommendation

Plain Kotlin, with :core kept clean enough that a switch later is a build change, not a rewrite. The one thing that would change this is wanting the browser simulator to run the real pipeline.

Cost of switching later

iOS may follow once the Android prototype is good. Kotlin Multiplatform supports iOS (shared Kotlin compiles to a native framework), so the question is what a later switch from plain Kotlin costs. The build side is mechanical: the multiplatform plugin, src/commonMain, the targets; about a day. The rest is every use of a JVM-only API:

JVM-only Where it would show up Multiplatform replacement
java.text.Normalizer Accent folding in the normalizer’s lookup key A small platform implementation, or a library
java.security.MessageDigest The evidence fingerprint A multiplatform hashing library
java.time Retrieval times, cache lifetimes kotlin.time, and kotlinx-datetime for calendar dates
OkHttp engine :net Ktor’s Darwin engine on iOS; configuration only
SQLite JDBC driver :cli SQLDelight’s native driver; :store already takes an injected driver
JUnit 5 and MockK Every test kotlin.test and hand-written fakes

With the rules in place from the start (see Multiplatform readiness in the architecture), the switch is about a week: the build, two or three small platform implementations, and a test run on the iOS target. Without them it grows with the JVM-only usage, likely one to three weeks, most of it in tests.

iOS input

Sharing the pipeline does not make an iOS app. iOS does not let an app read what another app is playing: MPNowPlayingInfoCenter lets an app publish its own now-playing information, not read anyone else’s, and the private MediaRemote framework has never been available to App Store apps. On macOS, where some utilities used it, Apple restricted it to entitled system processes in macOS 15.4, which suggests the direction. A change looks unlikely; the one plausible lever is EU interoperability regulation, which so far targets connected devices, not this. An iOS version would need another input:

Input Gives Catch
Spotify Web API, currently-playing The current Spotify track and its playlist Spotify only; needs the Spotify account, optional on Android; five users per developer app
Apple MusicKit The current Apple Music track Apple Music only
ShazamKit Recognizes the song by listening Uses the microphone, against the “metadata only, never audio” decision
Manual search A track the user picks No “now playing”

So an iOS version is a different product shape, decided when it comes up; the shared pipeline is what carries over.

Decision

Plain Kotlin, kept Multiplatform-ready (owner, 2026-10-12).

D3. Small clients or vendor SDKs for the AI providers

The app needs two calls per provider: stream a completion, and list the available models.

Small clients

Pros:

Cons:

Vendor SDKs

Pros:

Cons:

Recommendation

Small clients. The surface used is small and stable, and keeping network access in :net is one of the design’s structural constraints. If one provider later needs a feature that is painful to hand-roll, adding its SDK just for that is a contained exception.

Cost of switching

Either way the switch stays inside :ai: the pipeline sees only AiProvider (stream and models), so a switch rewrites one implementation per provider and nothing else. The SDK side applies to Anthropic, OpenAI and Google; Ollama has no official JVM SDK and stays a small client either way.

Switch Code, per provider What else it costs
Small clients to an SDK About 1 to 2 days: implement AiProvider on the SDK, map its stream events and errors The SDK’s own HTTP stack bypasses :net, so rate limits, timeouts and key redaction are configured again inside it, and checkModuleGraph needs an exception. Its dependencies go through the allowlist and the licence check. Replay tests need a mock server instead of MockEngine. The official SDKs are JVM libraries, which breaks the Multiplatform readiness of D2
An SDK to small clients About 1 to 3 days: request and response models, the streaming parser, error mapping, recorded streams for the tests Nothing; the dependency and the exceptions go away

The code cost is similar both ways. The difference is that small clients keep the network boundary and Multiplatform readiness by construction, while each SDK has to be patched around both.

Decision

Small clients (owner, 2026-10-12). An SDK may still be adopted for one provider, if a feature it offers is painful to hand-roll, as a contained exception: confined to :ai, with its own checkModuleGraph and allowlist entries. After a Multiplatform switch it would move to the JVM and Android source sets, and that provider would be unavailable on iOS.