komi-store Documentation

A free, open-source app store for developers' releases on GitHub, Codeberg & Forgejo — browse, discover, and install apps with one click. Formerly GitHub Store.

RAW Doc

Technical Documentation: kurikomi-labs/komi-store

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)


[!CAUTION]
Free and Open-Source Android is under threat. Google will turn Android into a locked-down platform, restricting your essential freedom to install apps of your choice. Make your voice heard – keepandroidopen.org.

1 2 3 4 5 6

Download

[!IMPORTANT]
macOS Users: You may see a warning that Apple cannot verify Komi Store. This happens because the app is distributed outside the App Store and is not notarized yet. Allow it via System Settings → Privacy & Security → Open Anyway, or run xattr -dr com.apple.quarantine /Applications/Komi-Store.app after installing.

Scoop

powershell
scoop bucket add komi-store https://github.com/komi-store/komi-store-scoop-bucket
scoop install komi-store/komi-store

WinGet

powershell
winget install zed.rainxch.githubstore

[!TIP]
macOS Users: Install Komi Store with Homebrew.

Homebrew

bash
brew tap komi-store/komi-store
brew install --cask komi-store

Features

  • Smart discovery
  • Recently Released
  • Release browser & installs
  • Download mirror system
  • Rich details screen
  • App management
  • Collections
  • Tweaks
  • Localization (Available in 14 languages)
  • Network & performance

Team

rainxchzed illumiat KelvinCrag YumeYuka

Join Community


APK Signing Certificate

SHA-256:
B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA


Business Inquiries

Komi Store is open to partnerships, sponsorships, and integrations.

If you're interested in working together, reach out:

📧 Email: [email protected]
💬 Discord: Join our community


Disclaimer

Komi Store only helps you discover and download release assets that are already published on
GitHub, Codeberg and other forges by third‑party developers.
The contents, safety, and behavior of those downloads are entirely the responsibility of their
respective authors and distributors, not this project.

By using Komi Store, you understand and agree that you install and run any downloaded software at
your own risk.
This project does not review, validate, or guarantee that any installer is safe, free of malware, or
fit for any particular purpose.


Code Style (`docs/code-style.md`)

Komi Store code style

Status: adopted on 2026-10-19 from the decisions in findings.md. Some rules get their automated check in follow-up work; until then they apply to new and touched code. Links point at examples in cfa6bd30.

This file is for people and agents writing code here. CLAUDE.md files point to it. A tag after a rule says what enforces it:

  • [Spotless]: ./gradlew spotlessCheck in CI: ktlint 1.8.0 with the Compose rules, on files changed against main
  • [detekt]: ./gradlew detekt in CI, with per-module baselines
  • [Konsist]: ./gradlew :konsist-test:test in CI
  • [review]: maintainer review, and agents' own review before opening a PR; the mechanical ones move to Konsist in follow-up work

When a rule and nearby code disagree, follow the rule. Fix the nearby code only if your change already touches it.

1. Formatting and files

  • ktlint with the Compose rules decides formatting. Run ./gradlew spotlessApply before pushing; nothing formats on compile. [Spotless]
  • No wildcard imports, including res.*. ktlint's no-wildcard-imports is still disabled in .editorconfig. [review]
  • One top-level public type per file, named after it. [review]
  • Production files stay under 800 lines. Today's larger files are on an allowlist that can only shrink. [Konsist]

2. Comments

  • No KDoc, no comments that restate the code. [review]
  • Comment only a non-obvious invariant, tricky concurrency or a workaround, in one or two lines. Good: AppsRepositoryImpl.kt:300-301, BackgroundRequest.kt:13-14.
  • No em-dashes anywhere: code, strings, logs, comments. [review]

3. Modules and packages

  • Packages are zed.rainxch.<feature>.<layer>, e.g. zed.rainxch.devprofile.data (hyphens dropped). [Konsist]
  • Common domain code imports no Ktor, Room, Compose, lifecycle, Koin or Android types. Platform actuals may. [Konsist]
  • Presentation never imports a data package. [Konsist]
  • Repository interfaces live in domain, XRepositoryImpl in data. [review]
  • No java.* or android.* in commonMain. Bad: AppsViewModel.kt:74. [review]

4. ViewModels

The recommended shape, already used by 11 ViewModels (AppsViewModel.kt:113-128, StarredReposViewModel.kt:39-55):

kotlin
class FooViewModel(
    private val repository: FooRepository,
) : ViewModel() {
    private var hasLoadedInitialData = false

    private val _state = MutableStateFlow(FooState())
    val state =
        _state
            .onStart {
                if (!hasLoadedInitialData) {
                    load()
                    hasLoadedInitialData = true
                }
            }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), FooState())

    private val _events = Channel<FooEvent>(Channel.BUFFERED)
    val events = _events.receiveAsFlow()

    fun onAction(action: FooAction) {
        when (action) {
            FooAction.OnRefresh -> load()
        }
    }
}
  • FooAction and FooEvent are sealed. [Konsist]
  • One public input: onAction. No other public functions. [review]
  • Update state with _state.update { }, never _state.value = _state.value.copy(...). [review]
  • Combine flows with combine, not several collectors writing shadow vars. Bad: HomeViewModel.kt:64-86. [review]
  • Route arguments come from the route object or one typed params class, never positional parametersOf (A8). Bad: ViewModelsModule.kt:47-52. [review]

5. State

  • A data class FooState with defaults, ImmutableList / ImmutableSet for collections. [review]
  • No @Stable / @Immutable on screen state; keep @Stable for real state holders like KomiToastState.kt:8. [review]
  • Derive values in one place before the UI sees them. Good: RawDetailsStateMapper.kt:11. [review]
  • No sentinel strings or nullable fields used as flags. Bad: AppsViewModel.kt:880. [review]
  • Errors in state are typed, not String. [review]

6. Errors and cancellation

  • Never wrap suspend calls in runCatching; it swallows CancellationException. [review]
  • A broad catch around suspend code rethrows CancellationException first. Good: DetailsViewModel.kt:208-212. [review]
  • Throwable.message never reaches the user. Map typed errors to resources at render time. Good: the typed cases in MirrorPickerRoot.kt:240-245 (its fallback case on the next line still shows exception text). Bad: DetailsViewModel.kt:3009. [review]
  • Fallible repository calls return Result<T> with typed domain exceptions. A typed outcome to copy: ProxyTestOutcome.kt:3-27.
  • Log with the injected KomiStoreLogger and pass the throwable, never only .message. [review]

7. Navigation

  • Routes are @Serializable members of GithubStoreGraph (GithubStoreGraph.kt:5). [review]
  • A click that only navigates is a lambda on the Screen (onNavigateToDetails), not an Action. [review]
  • The ViewModel sends a Navigate* event only when navigation depends on async work. [review]
  • No empty -> Unit branches for navigation in onAction. Bad: HomeViewModel.kt:212. [review]

8. Composables

  • A public FooRoot(callbacks, viewModel = koinViewModel()) collects with collectAsStateWithLifecycle, handles events with ObserveAsEvents, and calls a private stateless FooScreen(state, onAction, modifier). Closest today: ImportStarsRoot.kt:64-88, which still lacks the modifier parameter, as every route-level screen does. [review]
  • Every composable that emits UI takes modifier: Modifier = Modifier as its first optional parameter. [Spotless]
  • Pass leaf components the values they need, not the whole screen state. Bad: SmartInstallButton.kt:91-99. [review]
  • No business logic, koinInject, or dispatcher switching inside composables. Bad: About.kt:98-100, About.kt:140-149. [review]
  • Lazy list items have stable keys. [review]
  • Anything named rememberX calls remember. Bad: MarkdownUtils.kt:17. [review]

9. Design system

  • Use Komi* components and LocalPersonality.current tokens. No MaterialTheme.*, no Color(0x...) literals, no isSystemInDarkTheme(). Bad: DetailsRoot.kt:305, ExpandableDetails.kt:89. [review]
  • Before writing a component, look in core/presentation. Shared needs (empty and error states, favourite toggle, search bar, sort dropdown) belong there, not in a feature. [review]
  • New core components dispatch on the personality with private Manga / Classic implementations, like KomiButton.kt:70-123. [review]
  • Public core components and stateless screens have a preview wrapped in PersonalityPreview (PersonalityPreview.kt:16). [review]

10. Strings and accessibility

  • All user-visible text comes from Res.string, content descriptions included. Bad: KomiChip.kt:255. [review]
  • Counts use ``, not "(s)". [review]
  • Toggles and selectable rows set Role and stateDescription; error states announce themselves with liveRegion. [review]
  • New keys go into the default strings.xml with a feature prefix (tweaks_...). [review]

11. Data layer and coroutines

  • DTOs live in dto/; mappers live in mappers/ as toDomain() extensions. [review]
  • Use the injected HTTP clients. No HttpClient {} in feature code: those skip the proxy and are never closed. Bad: FeedRepositoryImpl.kt:41. [review]
  • Long-lived work runs on the injected app scope, not a private CoroutineScope in a singleton. [review]
  • No runBlocking outside app startup. [review]

12. DI

  • Data modules declare single<FooRepository> { FooRepositoryImpl(named = get()) }. [review]
  • ViewModels use viewModelOf(::FooViewModel) unless they take route arguments. [review]
  • No stateful object singletons outside DI, and no named("test") for production clients. Bad: ProxyManager.kt:23. [review]

13. Tests

  • kotlin.test, hand-written fakes, no mocking library. [review]
  • Test names are snake_case sentences, like UpdateVerdictTest.kt:62. [review]
  • Shared fakes live in one place per module instead of one copy per test file. [review]
  • New ViewModels come with tests once kotlinx-coroutines-test and Turbine are in the version catalog (follow-up).

14. Docs

  • CLAUDE.md files state only what the code can't tell you in a minute. [review]
  • Change a CLAUDE.md in the same PR as the code it describes. [review]

Agents Architecture (`docs/agents/architecture.md`)

Architecture

Clean Architecture with MVVM. Domain holds contracts, models and pure logic. Data implements them with Ktor, Room and Koin. Presentation holds ViewModels (StateFlow state, Channel events) and Compose UI. The ViewModel and screen shapes are in docs/code-style.md, sections 4 and 8.

Modules

text
composeApp/            entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)
core/domain            interfaces, models, use cases (no framework deps)
core/data              repositories, Ktor, Room, Koin, platform implementations
core/presentation      Personality design system (Classic / Manga), Komi* components, 14-locale strings
feature/               apps auth details dev-profile favourites feed home profile recently-viewed repo-pages search starred tweaks
konsist-test/          architecture rules
build-logic/convention convention plugins

Each feature has up to three sub-modules: domain/, data/, presentation/. profile, recently-viewed and tweaks are presentation-only. favourites and starred also have domain/ and data/ modules, currently empty.

Routes are members of the @Serializable sealed interface GithubStoreGraph in composeApp/.../app/navigation/GithubStoreGraph.kt; AppNavigation.kt wires them. Two route arguments matter beyond their names:

  • DetailsScreen.packageName (set from Apps) opens Details on that installed app when a repository ships several apps.
  • DetailsScreen.sourceHost is non-null for Codeberg, Forgejo and custom-forge repositories and routes every DetailsRepository call through ForgejoClientRegistry instead of the GitHub-backed path.

RepoPullsScreen is registered but nothing navigates to it yet.

DI

Koin. Each feature's data module declares its bindings in data/di/SharedModule.kt. ViewModels are registered in composeApp/.../app/di/ViewModelsModule.kt (viewModelOf(::X), or viewModel { ... } when they take route arguments). Modules are assembled in initKoin.kt.

Core repositories (`core/domain`)

FavouritesRepository, StarredRepository, InstalledAppsRepository, SeenReposRepository, HiddenReposRepository, TweaksRepository, UserSessionRepository (signed-in user, token, session expiry), ProxyRepository, RateLimitRepository, ExternalImportRepository, MirrorRepository, AnnouncementsRepository, CacheRepository, BrowseFilterStore, HostTokenRepository. SearchHistoryRepository lives in feature/search/domain.

System interfaces with Android and Desktop implementations: Installer, InstallerStatusProvider, PackageMonitor, SystemInstallSerializer.

Convention plugins (`build-logic/convention/`)

convention.kmp.library (domain and data), convention.cmp.library (core/presentation), convention.cmp.feature (feature presentation), convention.cmp.application (the app), convention.room, convention.buildkonfig, convention.quality (detekt and Kover, applied by the library and app conventions). The Compose conventions also apply the stability analyzer; Android Lint is configured in configureKotlinAndroid.

Adding a feature

  1. Create feature/<name>/{domain,data,presentation}/ with the matching convention plugin.
  2. include the modules in settings.gradle.kts.
  3. Domain interfaces, then the implementation and its Koin module in data/di/SharedModule.kt, then the ViewModel and screen.
  4. Add the route to GithubStoreGraph.kt, wire it in AppNavigation.kt, register the ViewModel in ViewModelsModule.kt and the data module in initKoin.kt.
  5. Add a feature/<name>/CLAUDE.md with what a reader can't see from the code in a minute.

Agents Checks (`docs/agents/checks.md`)

Checks

CI (.github/workflows/pr-checks.yml) runs on every pull request and every push to main. Existing findings live in committed baselines, so a check fails only on new issues.

bash
./gradlew spotlessApply                                   # format files changed vs origin/main (ktlint 1.8.0 + Compose rules + .editorconfig)
./gradlew detekt                                          # static analysis; baseline: <module>/detekt-baseline.xml
./gradlew lintDebug                                       # Android Lint; baseline: <module>/lint-baseline.xml
./gradlew jvmTest koverXmlReport                          # tests and merged coverage (build/reports/kover)
./gradlew compileDebugKotlinAndroid; ./gradlew debugStabilityCheck   # Compose stability vs <module>/stability/*.stability
./gradlew :konsist-test:test                              # architecture rules
./gradlew spotlessInstallGitPrePushHook                   # opt-in pre-push format check

CodeQL (codeql.yml) and Dependabot (dependabot.yml) run separately.

Spotless

Spotless checks only files changed against origin/main, but it checks the whole file. The codebase was formatted once in #1080 (listed in .git-blame-ignore-revs). Compose rule findings (missing modifier, parameter order, naming) are not auto-fixable and have to be fixed by hand in files you touch.

Baselines

ktlint findings the formatter can't fix (mostly Compose rules) are listed per file and rule in config/ktlint/baseline.txt, and Spotless suppresses exactly those. When you fix one, delete its line.

Refresh a baseline only for an intentional change: detektBaseline, updateLintBaseline or debugStabilityDump. The stability analyzer (0.7.0) shares one output file across a module's compilations, so run its tasks in their own Gradle invocation right after compileDebugKotlinAndroid (fixed upstream only in releases built for Kotlin 2.4). Kotlin must match the stability analyzer's Kotlin version (see its version map) when bumping either.

Warnings as errors

Kotlin warnings are errors (allWarningsAsErrors). A module can opt out with komi.warningsAsErrors=false in its own gradle.properties; none does.

Konsist

The rules and their allowlists live in konsist-test/src/test/kotlin/zed/rainxch/konsist/. An allowlist can only shrink: the file-size test fails when an allowlisted file drops under the limit, so remove the entry in the same PR.

DocsRulesTest reads AGENTS.md, CLAUDE.md, CONTRIBUTING.md, docs/agents/*.md and feature/*/CLAUDE.md. A backticked route such as GithubStoreGraph.DetailsScreen must exist in the graph, and any backticked name ending in Screen or Graph must still exist in production code. When you rename a route or screen, update the docs in the same PR.

Agent config

The Agent config job runs agnix over the repository (AGENTS.md, the CLAUDE.md files, .claude/ and the rest) with its default rules, and fails only on errors. It runs in CI only.


Agents Distribution (`docs/agents/distribution.md`)

Distribution (maintainers)

Desktop installers are built by .github/workflows/build-desktop-platforms.yml on every push to the generate-installers branch. Its release job reads projectVersionName and creates the draft release v<version>.

Windows signing (SignPath Foundation)

The sign-windows job runs after every push to generate-installers. The SignPath action is pinned to a commit SHA. Secrets: SIGNPATH_API_TOKEN, SIGNPATH_ORGANIZATION_ID (1ecf111e-...). The variable SIGNPATH_SIGNING_POLICY_SLUG is test-signing until the production certificate is issued, then release-signing. Project slug GitHub-Store, artifact configuration slug initial. The unsigned artifact is deleted after signing, so only windows-installers-signed reaches the draft release.

WinGet

.github/workflows/winget-publish.yml fires on release: [released] and uses vedantmgoyal9/winget-releaser to publish the MSI (installers-regex: '\.msi$') as zed.rainxch.githubstore. The secret WINGET_TOKEN is a PAT with Contents and Pull requests write access on komi-store/winget-pkgs (a fork of microsoft/winget-pkgs). The workflow doesn't set fork-user; the action defaults it to the repository owner, komi-store.

Scoop

.github/workflows/scoop-publish.yml fires on release (or by hand) and dispatches komi-store-release to komi-store/komi-store-scoop-bucket with the secret SCOOP_BUCKET_TOKEN.

macOS (Homebrew)

The cask lives in the tap komi-store/komi-store (repository homebrew-komi-store): brew install --cask komi-store. The app is unsigned for now, so users run xattr -dr com.apple.quarantine /Applications/Komi-Store.app after installing. CI builds .dmg and .pkg on every push to generate-installers. On release, .github/workflows/homebrew-tap-publish.yml updates Casks/komi-store.rb in the tap with the secret HOMEBREW_TAP_TOKEN; it can also be run by hand for a given tag.


Agents Forges (`docs/agents/forges.md`)

Codeberg, Forgejo and Gitea

Forge repositories are fetched directly from the forge, with no backend in between.

  • Clients: one ForgejoApiClient per host (60 s request, 30 s connect and socket timeouts, exponential retry on 5xx and IOException). ForgejoClientRegistry.clientFor(host) caches them behind a Mutex; it is proxy-aware and drops cached clients when the proxy changes.
  • IDs: RepoIdCodec packs a 23-bit host fingerprint and the 40-bit raw id into the existing 64-bit repoId slot (sign bit set = foreign source), so the GitHub-shaped schema stays as it is.
  • URLs: RepositoryUrlParser recognises GitHub, Codeberg, gitea.com, git.disroot.org and user-added forge hosts.
  • README: /contents/README.md?ref={branch}; Forgejo has no /readme endpoint.
  • License: the first of LICENSE, LICENSE.md, LICENSE.txt and COPYING found under /contents/, matched against SPDX headers.
  • Downloads: the sum of asset.download_count across releases.
  • Details: a non-null DetailsScreen.sourceHost routes every DetailsRepository call through ForgejoClientRegistry.

Per-host access tokens

HostTokenRepository stores HostToken(host, token, displayName, createdAtEpochMillis) entries as one JSON list under a single KSafe key (AES-256-GCM). Tweaks, Access Tokens (HostTokensScreen) adds, validates and deletes them. HostTokenInterceptor (a Ktor plugin on every ForgejoApiClient and the GitHub-direct client; never the backend, test or download clients) adds Authorization: token $pat to an https request whose host, with its port, matches a saved token, reading it per request so edits apply without a restart. An Authorization already set, such as the GitHub account sign-in, wins. HostNames.apiHostToTokenHost maps api.github.com, uploads.github.com and raw.githubusercontent.com to the github.com token. Ktor drops the header on cross-host redirects, and the mirror rewrite drops it when the host changes.


Agents Platform (`docs/agents/platform.md`)

Platform notes

Installing apps on Android

Silent installs go through Shizuku (ShizukuProvider, then AIDL, then pm install -S), Dhizuku, or root through libsu (services/root/RootServiceManager). services/installer/SilentInstallerDispatcher in core/data/src/androidMain picks one from the installer setting and falls back to the standard installer on failure.

Desktop logs

CrashReporter.install(), called at the start of DesktopApp.main, tees stdout and stderr to a rotating session.log and writes crash-<ts>.log on an uncaught exception.

OS Path
macOS ~/Library/Logs/Komi-Store/
Windows %LOCALAPPDATA%/Komi-Store/logs/
Linux $XDG_STATE_HOME/Komi-Store/logs/ (or ~/.local/state/Komi-Store/logs/)

Android logs go to Logcat.

Encrypted storage

KSafe provides AES-256-GCM, backed by the hardware Keystore on Android. Persisted credentials and preferences go through the extension functions in core/data/secure/KSafeSafe.kt (safeGet, safePut, safeDelete, safeGetFlow), which log and return null or false on a transient failure instead of throwing through coroutine scopes. A few raw calls remain, for example in HostTokenRepositoryImpl.

Gradle

Configuration cache and build cache are on. The Gradle daemon gets 4 GB of heap and the Kotlin daemon 3 GB (gradle.properties). Library versions live in gradle/libs.versions.toml.


Agents Releases And Assets (`docs/agents/releases-and-assets.md`)

Releases, assets and installed apps

How a release asset is matched to the platform and to an installed app. All of this is pure logic in core/domain/utils, shared by Details and the update checker, with tests in core/domain/src/commonTest (start with UpdateVerdictTest). AssetMatchingReplayTest in core/domain/src/jvmTest replays a corpus of real repositories collected by tools/asset-replay/.

  • AssetVariant: token, glob and stem fingerprints of an asset name, plus versionMajor. tagGlob blanks a name's own release version and adjacent build ids, so callers pass each name's release tag.
  • AssetOwnership: which installed app owns an asset when a repository ships several apps. Filter first, then glob, then stem; ties go to the matching major version.
  • assetPlatformOf: the platform an asset is for.
  • newestReleasePerPlatform: the newest release for each platform, the device's platform first. Drives the Details platform chips.
  • ReleaseLines: which app a tag belongs to in a monorepo (auth-v4.4.25 belongs to auth). Renames and Details chips stay within a line.

DetailsScreen.packageName, set when Details opens from Apps, selects the installed app in a multi-app repository.

In the Library, an update or re-download starts from the match the last update check stored on the installed app (latestReleaseId and the latestAsset* fields). A fresh release lookup runs only when nothing is stored or a download from the stored match failed (AppsViewModel.resolveUpdateTarget).


Agents Sign In (`docs/agents/sign-in.md`)

Sign-in and the backend

GitHub OAuth

GITHUB_CLIENT_ID comes from local.properties; without the key, the build uses the built-in client id (BuildKonfigConventionPlugin). An empty value overrides it and breaks sign-in. Deep links: githubstore://auth (web-OAuth handoff), githubstore://callback (leftover from the device flow), githubstore://repo, githubstore://apps; the full set is in DeepLinkParser.kt.

Sign-in flow (web OAuth first)

  1. Web OAuth with PKCE and a handoff. feature/auth/data/crypto/PkceGenerator mints (state, codeVerifier, codeChallenge). WebAuthApi.register POSTs verifier, challenge and state to https://github-store.org/auth/register (a Cloudflare Worker stores them in Workers KV) and returns authUrl. The user authorizes on github.com, GitHub redirects to github-store.org/auth/callback?code&state, and the Worker exchanges the code via api.github-store.org. The backend stores (handoffId -> access_token) for 60 s in Postgres with an atomic DELETE ... RETURNING, then bounces back to githubstore://auth?handoff=<handoffId>&state=<state>. The app reads the handoff with WebAuthApi.consumeHandoff (GETDEL semantics).
  2. Device flow through the backend's /v1/auth/device/start and /poll. AuthPath (Backend or Direct) is kept in SavedStateHandle and only escalates from Backend to Direct on infrastructure errors.
  3. Personal access token pasted by the user (signInWithPat): validated against /user, and saved optimistically when GitHub is unreachable.

Backend rate limits: 10 device starts and 200 device polls per hour per IP. Endpoints are in core/data/network/BackendEndpoints.kt (BACKEND_ORIGIN, WEB_ORIGIN).

The `X-GitHub-Token` header

The client attaches the user's token as X-GitHub-Token when TokenStore.currentToken() is non-null on /v1/search, /v1/search/explore, /v1/repo (including POST .../refresh), /v1/releases, /v1/readme, /v1/user and /v1/users/{user}/repos. The backend re-sends it as Authorization: token $token to GitHub; without it, the backend round-robins a pool of 4 service tokens.

  • An upstream 401 comes back as a backend 502 and is treated like "GitHub unreachable": the client falls back through shouldFallbackToGithubOrRethrow.
  • A 429 gets no fallback (the same limit applies), only backoff.
  • UnauthorizedInterceptor sits only on the direct-GitHub client; UserSessionRepositoryImpl debounces consecutive 401s by token snapshot.

Agents Translation (`docs/agents/translation.md`)

Translation providers

The TranslationProvider enum has GOOGLE, YOUDAO, LIBRE_TRANSLATE, DEEPL and MICROSOFT. Each provider's settings are stored through TweaksRepository (KSafe-encrypted), and TranslationRepositoryImpl.resolveTranslator() picks the implementation.

  • LibreTranslate uses the bundled translate.disroot.org mirror when the user's URL setting is blank.
  • DeepL sends keys ending in :fx to api-free.deepl.com.
  • Microsoft uses No-Trace by default: text is never stored and never used for training.

Quality Findings (`docs/quality/findings.md`)

Code quality research: Komi Store and kotlinconf-app

Status: decisions made on 2026-10-19 (section 5). The rules they produce live in docs/code-style.md; this file keeps the evidence.

  • Komi Store: origin/main at cfa6bd30 (2026-10-18). Links into our code are permalinks to that commit, so they stay correct as the code moves.
  • kotlinconf-app: 2189155, links are pinned to it.

How this was made

  • 8 read-only research agents walked both codebases on the same 15 dimensions: 2 on kotlinconf-app (non-UI and UI), 6 covering all 13 Komi features, core/*, composeApp, build-logic and CI.
  • Lint numbers come from actually running ktlint 1.8.0 with the Compose rules on all 37 modules in a scratch worktree (section 2).
  • Every link was checked twice. A script confirmed that each file and line exists, then three verifier agents re-read all 277 citations against the code: 259 were right as written and 18 were corrected. The bugs in section 3 marked "verified" were also re-read by hand.
  • Agents can still misread code. Treat a single citation as a pointer, not proof, until someone looks.

TL;DR

  1. kotlinconf-app is a source of techniques, not a quality bar. It has no lint, two client test files, a 587-line god service, Boolean flags for one-off events and no stateless screen layer. Copying it wholesale would lower our bar in several places (section 1).
  2. Our biggest problem is internal drift, not missing kotlinconf patterns. Five ViewModel shapes, three ways to navigate, three repository error contracts, no rule for user-facing text, and CancellationException swallowed in dozens of places (section 7).
  3. Lint covers 1 of 37 modules. With ktlint on everywhere, hand-written code has 6,184 violations. A one-time ktlintFormat fixes all but 201 of them and touches 551 files (section 2).
  4. The CLAUDE.md files describe code that no longer exists: routes, repositories, components, injected dependencies, cache TTLs. Agents read them on every task. Fixing the docs is the cheapest high-leverage step (section 8).
  5. The research found real bugs, listed apart from style in section 3: per-host access tokens are never sent (regression since #666), the Desktop database is wiped on the next schema bump, a rate-limit exception can escape into viewModelScope, and more.

1. What kotlinconf-app is, next to us

Aspect kotlinconf-app Komi Store
Modules :core, :app:ui-components, one :app:shared with every screen (settings.gradle.kts:35-44) 37 modules, features split into domain / data / presentation
Build logic none; the Android block is copied in 3 build files (build.gradle.kts:25-31) 9 convention plugins in build-logic
DI Metro 1.4, compile-time graphs (YearGraph.kt:10-22) Koin 4.1
Navigation Navigation 3 with per-tab back stacks (NavState.kt:22-28) Navigation Compose 2.9, one graph (GithubStoreGraph.kt:5)
ViewModel API several StateFlows plus public methods (ScheduleViewModel.kt:111) mostly State + onAction(sealed) + Channel events
One-off events Boolean StateFlow plus a reset call (FeedbackViewModel.kt:37-38) Channel + ObserveAsEvents (ObserveAsEvents.kt:13)
Loading and error UI one generic ErrorLoadingState and one renderer (ErrorLoadingContent.kt:21-53) nothing shared; about 19 feature-local Loading/Error/Empty composables
Screen structure screen takes the ViewModel as a default parameter, no stateless layer (ScheduleScreen.kt:87-93) XRoot + stateless XScreen(state, onAction) in most features
Design system custom, built on foundation, no Material (KotlinConfTheme.kt:41-56) custom Personality system on top of Material 3
Previews 79, all in ui-components, none for screens 75 in core/presentation, a handful in features
Static analysis none ktlint on composeApp only
Tests 2 client test files, 242 lines 35 test files, strong pure-logic tests, no ViewModel tests
CI builds every platform, runs jvmTest (build-all.yml:25-27) packaging only, nothing runs on pull requests

What to take from it is a handful of specific techniques (section 4), not its architecture. Several of its own weak spots are listed by the agents: a god service that ViewModels depend on concretely (ConferenceService.kt:46), ViewModels passed into child composables (ScheduleScreen.kt:311), a global navigation channel (NavHost.kt:91), and empty results shown as errors (ScheduleViewModel.kt:166-167).

2. Measurements

Lint

What Number
Modules ktlint runs on today 1 of 37 (composeApp, through AndroidApplicationComposeConventionPlugin.kt:13)
Violations in hand-written code, ktlint on everywhere 6,184 in 588 files
Top rules multiline-expression-wrapping 1,335; function-signature 898; indent 622; chain-method-continuation 395; when-entry-bracing 393; class-signature 329
Worst modules core/data 1,365; feature/tweaks 850; core/domain 744; feature/apps 707; feature/details 662
Diff of a one-time ktlintFormat 551 files, +16,094 / -12,334 lines
Left after that format 201 in 132 files: 170 Compose rules, 31 standard rules
Is the format safe? Yes: a trial run compiled for JVM and Android debug and all 411 JVM tests passed
What the 201 are compose:modifier-missing-check 60, compose:parameter-naming 35, compose:param-order-check 23, standard:package-name 13 (snake_case packages), compose:lambda-param-in-effect 10, others under 10 each

Run details: ktlint 1.8.0 (already pinned in KtlintConventionPlugin.kt:11), the project .editorconfig, plus io.nlopez.compose.rules:ktlint:0.6.0.

Three things came out of making the run work. The first two concern ktlint-gradle, which #1056 replaces with Spotless; the third still decides which Compose rules version works:

  1. The generated-code filter does not work. KtlintConventionPlugin.kt:15 excludes **/generated/**, but ktlint-gradle matches patterns relative to each source root, so KSP Room code and Compose resource accessors were linted too: 104,706 extra violations from generated files. Excluding by absolute path (it.file.invariantSeparatorsPath.contains("/build/")) fixes it.
  2. The format-on-build hook breaks once ktlint is on everywhere. build.gradle.kts:21-34 makes preBuild and compile depend on ktlintFormat. In core:data that creates a task cycle through KSP (preBuild -> ktlintFormat -> kspDebugKotlinAndroid -> preDebugBuild). It also rewrites sources during a compile.
  3. compose-rules 0.6.1 and later crash on ktlint 1.8.0. They are built with Kotlin 2.4 and call a stdlib function the ktlint worker does not have (NoSuchMethodError: sequenceOf). 0.6.0 is the newest release built on Kotlin 2.2.21 and runs fine.

Detekt

Measured on cfa6bd30 with detekt 1.23.8:

What Number
Default config 3,040 findings in 467 files. MagicNumber 785, FunctionNaming 626 (composables are PascalCase), TooGenericExceptionCaught 332, LongMethod 244
A Compose-aware config (prototype, not committed) 1,008 in 223 files. MagicNumber 463 (non-UI code), ReturnCount 88, LongMethod 83, CyclomaticComplexMethod 80, SwallowedException 26, EmptyCatchBlock 4, PrintStackTrace 4
Longest functions the NavHost lambda in AppNavigation.kt:92 (899 lines), TweaksViewModel.onAction TweaksViewModel.kt:561 (822), preprocessMarkdown.kt:3 (408), DetailsRoot.kt:106 (396), SearchRoot.kt:237 (383), AppsRoot.kt:214 (355)

Detekt 1.23.8 is built on Kotlin 2.0.21; running it in our Gradle build needs its Kotlin dependencies pinned to that version. #1056 runs it in CI with per-module baselines, so only new findings fail a PR (D12).

Tests, CI, review

What Number
Test files 35. core/domain has 289 tests (UpdateVerdictTest alone 60, plus a 48-project corpus replay); core/data 11 files, mostly downloads; core/presentation 2; feature/apps/presentation 2; feature/details/domain 1
ViewModel tests 0
Test libraries in the version catalog none; modules add kotlin("test") one by one (build.gradle.kts:15)
CI that runs on pull requests none at cfa6bd30 (build-desktop-platforms.yml:3-6 runs on pushes to generate-installers); pr-checks.yml (#1056) now compiles JVM and Android debug, runs jvmTest with coverage, Spotless, detekt, Android Lint, the Compose stability check and Konsist
What CONTRIBUTING says "CI lints every PR" (CONTRIBUTING.md:153) and "No CI runs against PRs" (CONTRIBUTING.md:251)
CodeRabbit dropped on 2026-10-19 and its config removed. It used to read every **/CLAUDE.md as review guidelines, so stale docs misled it too.

Size hot spots

File Lines
DetailsViewModel.kt 3,278 (22 dependencies, an onAction of about 400 lines)
AppsViewModel.kt 2,446 (12 dependencies, 67 action types in one when)
TweaksViewModel.kt 1,722 (86 launch calls)
DesktopInstaller.kt 1,387
SearchRoot.kt 1,119
AppNavigation.kt 1,035 (every destination inline in one NavHost)

3. Bugs and risks found on the way

Not style. Each deserves an issue or a small fix PR of its own, separate from the style work.

# What Where Status
1 Per-host access tokens are never sent. HostTokenInterceptor was added in #634 (aa5de4c0) and deleted in #666 (c94f50b2). Since then HostTokenRepository is read only by the Tweaks screen and the Forgejo client sets no Authorization header. Users who add a Codeberg/Forgejo token get no effect. HostTokensViewModel.kt:28, ForgejoApiClient.kt:52-56 verified (git history + grep)
2 Desktop wipes its database on the next schema bump. Desktop uses fallbackToDestructiveMigration(true) with only MIGRATION_19_20 registered. Android registers the full chain. initDatabase.kt:22-23 verified
3 A rate-limit exception escapes into viewModelScope. getIssues returns Result but rethrows RateLimitException; IssuesViewModel calls it in launch with no catch, so the exception goes to the default handler (a crash on Android). RepoPagesRepositoryImpl.kt:75-76, IssuesViewModel.kt:138-142 verified
4 "Update all failed" shows an array identity. getString(Res.string.update_all_failed, arrayOf(e.message)) passes the array as one format argument. AppsViewModel.kt:1534-1537 verified
5 Most Tweaks events are dropped. Events not listed fall into else -> Unit; 11 of the 14 event types are dropped, including proxy save and proxy test errors, so the user gets no feedback. TweaksRoot.kt:108 verified
6 Developer profiles never show as organizations when loaded through the backend. isOrganization is derived from userType, which the mapper never sets. UserProfileNetworkToDomain.kt:6-24, DeveloperProfile.kt:20-22 verified
7 Search load-more may read stale flags. remember { derivedStateOf { ... state.isLoadingMore ... } } has no key and captures the first state parameter. SearchRoot.kt:246-272 likely; needs a device check
8 The same platform filter behaves differently. Feed sends "ios" for iOS, Home sends nothing. FeedRepositoryImpl.kt:148, CachedRepositoriesDataSourceImpl.kt:467 verified
9 Sign-in ignores the user's proxy. GitHubAuthApi builds its own HttpClient {} with no proxy configuration, unlike the injected clients. GitHubAuthApi.kt:37-53 verified
10 The auth ViewModel collects the token flow on the injected app scope, so the collector outlives the ViewModel. AuthenticationViewModel.kt:41, AuthenticationViewModel.kt:64 reported
11 Each Retry on Details starts another untracked observeInstalledApp collector. DetailsViewModel.kt:1197 reported
12 The developer-profile filter launches a new job per keystroke without cancelling the old one, so stale results can win. DeveloperProfileViewModel.kt:144 reported
13 Every GitHub request runs runBlocking to read the token. HttpClientFactory.kt:91, DefaultTokenStore.kt:68-69 reported
14 BootReceiver calls runBlocking on the main thread after goAsync(). BootReceiver.kt:15-19 reported
15 The external-installer dialog is unreachable: showExternalInstallerPrompt is never set to true. RawDetailsState.kt:77, DetailsRoot.kt:478 reported
16 19 string keys are missing in 11 locales (10 in the two Chinese ones), and whatsnew/22.json exists in only 2 of 14 locale folders. strings.xml:164 (pause) reported

Fixes: bug 1 in #1063, bug 2 in #1062, bugs 3 and 4 in progress; bugs 5 to 9 are to be filed as issues.

4. Adopt: cheap, clear wins

These need no big decision. Each says what kotlinconf does, where we stand, and the proposed rule.

A1. Never swallow cancellation

A2. One shared load / error / empty renderer

A3. Resolve user text at render; never show `e.message`

A4. Accessibility semantics in shared components

A5. Previews where they pay off

  • kotlinconf: one preview wrapper (PreviewHelper.kt:13-31) with androidx's @PreviewLightDark (77 previews) and one custom wide multipreview (3 previews, MultiPreviews.kt:7-11) make 79 previews cheap; a fake InteractionSource previews hover and focus (PreviewInteractionSource.kt:9-17). It has zero screen previews because it has no stateless screens.
  • Ours: the PersonalityPreview wrapper (PersonalityPreview.kt:16) renders one personality per call, and core components write one preview per personality: 75 previews in core. But 25 core component files have none, and features have 0 to 4 each (feed, home and recently-viewed have none). Our XScreen(state, onAction) split lets us preview whole screens, which kotlinconf cannot.
  • Rule: every public core component and every stateless XScreen gets at least one preview with fake state. Add the hover/focus trick for desktop.

A6. Power-assert for tests

  • kotlinconf turns on the Kotlin power-assert compiler plugin for assertEquals / assertTrue (build.gradle.kts:147-156), so failures print the values involved with no assertion library.
  • Rule: apply it to test source sets in a convention plugin.

A7. No wildcard imports

A8. Typed route arguments, no positional `parametersOf`

A9. Short, true CLAUDE.md files

  • kotlinconf's CLAUDE.md lists modules, commands and conventions that match the code (it only skips the small admin app module).
  • Ours describe things that are gone (section 8). Rule: CLAUDE.md files state only what a reader cannot get from the code in a minute, and are fixed in the same PR that changes the thing they describe.

A10. Pin every GitHub Action by commit SHA

Worth knowing, not rules

5. Decide: your call

Decided on 2026-10-19: option A for D1 to D11. D12 was overtaken: CodeRabbit is no longer used, detekt runs in CI with baselines through #1056, and the mechanical rules move to Konsist.

D1. One ViewModel shape

Today, at least five shapes exist:

Shape Examples
_state + onStart { if (!hasLoaded) load() } + stateIn(WhileSubscribed(5_000)) + onAction + Channel AppsViewModel.kt:113-128, DetailsViewModel.kt:174-192, TweaksViewModel.kt:93-128, 11 ViewModels in total
asStateFlow() + init { load() } (the shape root CLAUDE.md documents) ProfileViewModel.kt:21-33, CategoryListViewModel.kt:30, IssuesViewModel.kt:37-42
public methods or bare state, no Action or Event types DetailsAboutViewModel.kt:37-126, AppInfoViewModel.kt:8-12, WhatsNewViewModel.kt:24-31
onAction but no events PullsViewModel.kt, SecurityViewModel.kt
several public flows AnnouncementsViewModel.kt:38-64
  • Option A (recommended): the dominant first shape. State loads when the screen subscribes, not when the ViewModel is created, and it is already in 11 ViewModels. Name the 5_000 once. Add a Konsist test: every XViewModel exposes state: StateFlow<XState> and fun onAction(action: XAction).
  • Option B: kotlinconf's several flows plus methods. Less boilerplate for tiny screens, but it splits the API and pushes one-off events into Boolean flags.
  • Option C: keep the documented asStateFlow() + init shape. Simpler, but it loads on creation and would mean changing the 11 ViewModels that already use A.

D2. Error contract and user text

Repositories use three contracts today: kotlin.Result (Feed FeedRepositoryImpl.kt:54, repo-pages RepoPagesRepository.kt:16), thrown exceptions (Details DetailsRepository.kt:13), and silently emitting empty results (Home HomeRepositoryImpl.kt:477-486). One interface mixes Result with plain throwing calls (AuthenticationRepository.kt:10-23).

  • Option A (recommended):
    • Fallible repository calls return Result<T>, and failures are typed domain exceptions. RateLimitException already exists; add NotFound and Offline.
    • Cancellation is never caught.
    • ViewModels keep a typed error in state, and composables map it to a string resource. We already have one good model to copy: ProxyTestOutcome.kt:3-27.
  • Option B: a shared UiText (resource plus arguments, or a raw string) that ViewModels put in state. A common Android pattern, but it keeps string decisions in ViewModels and still invites UiText.Raw(e.message).
  • Option C: a project-wide sealed DataError hierarchy with its own Result type. The most type-safe, and the largest migration.

D3. One navigation rule

Three routes exist, sometimes on one screen:

On Apps, back and starred go one way and repo navigation the other (AppsRoot.kt:150-156 against AppsViewModel.kt:453-465).

  • Option A (recommended): clicks that only navigate are plain lambdas on the Screen (onNavigateToDetails), not Actions. The ViewModel emits a Navigate* event only when navigation depends on async work, for example after sign-in. No navigation cases inside Action types, so no -> Unit branches.
  • Option B: keep "Root intercepts the Action" everywhere. Least churn, but every navigation Action needs an empty ViewModel branch forever.

D4. How to roll ktlint out

Measured in section 2:

  • Option A (recommended):
    • One mechanical ktlintFormat PR at a quiet moment: 551 files, compile-checked in a trial run.
    • Add its commit to .git-blame-ignore-revs.
    • Fix or baseline the 201 that remain, then fail CI on new violations.
    • Cost: open PRs and volunteer branches need a rebase.
  • Option B: a ktlint baseline of all 6,184 violations and no mass format. ktlint baselines match on line and column, so any edit that shifts lines re-surfaces old violations in touched files. That causes noise for months.
  • Option C: format module by module over several PRs. Smaller rebases, but CI can only gate modules that are done.
  • Since then #1056 runs ktlint through Spotless on files changed against main, so formatting blocks from day one without baselines. The one-time format still matters: until it lands, touching a file means reformatting all of it in that PR.

D5. Remove the format-on-build hook

  • build.gradle.kts:21-34 rewrites sources during compile, hides violations from CI, and creates a task cycle in KSP modules once ktlint is on everywhere (section 2).
  • Recommended: remove it. #1056 does, and formats through spotlessApply with an opt-in pre-push hook.

D6. Compose lint rules

  • 170 findings after the format pass. Most are cheap: a missing modifier parameter on screens and components (60, for example DetailsRoot.kt:530), parameter naming (35), parameter order (23).
  • Recommended: on, fix the cheap ones in the same rollout, and turn off by .editorconfig any rule we disagree with (for example compositionlocal-allowlist, given our Local* design).

D7. One logging style

D8. A size ratchet for god classes

D9. Stability annotations

  • Strong skipping is the Compose compiler default on our Kotlin version. kotlinconf puts no stability annotations on state classes; its only @Stable is a state holder (NotificationBar.kt:17), the same split recommended here.
  • Ours are random: HomeState.kt:10 is @Stable, FeedState.kt:11 @Immutable, the apps states have none.
  • Recommended:
    • No @Stable or @Immutable on screen state classes. Keep ImmutableList in state, which we already do almost everywhere.
    • Keep @Stable on real state-holder classes like KomiToastState.kt:8.

D10. Test stack and the first ViewModel tests

  • No ViewModel tests and no coroutine test tooling exist today.
  • Recommended:
    • Add kotlinx-coroutines-test and Turbine to the version catalog, with fakes over mocks as now.
    • Make one small feature the reference: starred (1,089 lines) or favourites (1,402).
    • Write its ViewModel tests, then copy the pattern.

D11. Metro and Navigation 3

  • Recommended: not now.
  • Metro would mean migrating 33 ViewModels and about 100 Koin singletons. The real pain it would remove, positional parametersOf, is fixed more cheaply by A8.
  • Navigation 3's per-tab back stacks are attractive for the bottom bar and desktop sidebar, but Navigation Compose 2.9 works today.
  • Revisit both if DI bugs or tab-state complaints become real.

D12. Detekt: review comments or a CI gate

  • Resolved: CodeRabbit is no longer used. detekt runs in Gradle and CI with per-module baselines through #1056 (option B), and the mechanical rules this research wanted become Konsist tests.

6. Keep ours

What Why it beats or matches kotlinconf
Convention plugins in build-logic kotlinconf copies Android setup between modules (build.gradle.kts:15-20)
Feature modules split into domain / data / presentation keeps features apart; kotlinconf has one client module
XRoot + stateless XScreen(state, onAction) enables screen previews and tests; kotlinconf passes ViewModels into children (SessionScreen.kt:197)
Channel events + ObserveAsEvents kotlinconf's Boolean flags need manual resets and drop repeated values (StartNotificationsViewModel.kt:40)
ImmutableList in state fine with strong skipping; kotlinconf uses plain List
One shared strings.xml with 14 locales one place for translators; kotlinconf splits resources between two modules and duplicates 86 drawables
Personality design system with a public dispatcher and private Manga / Classic implementations richer than kotlinconf's theme, and features have almost stopped using raw Material (KomiButton.kt:70-123)
modifier as the first optional parameter on public core components matches the Compose API guidelines; kotlinconf is inconsistent (SpeakerCard.kt:32-33). Three of our markdown composables still take no modifier (AlertBlockQuote.kt:72, ExpandableDetails.kt:50, SyntaxHighlightedCode.kt:41)
Pure logic tests UpdateVerdictTest (60 tests) and the corpus replay go well beyond kotlinconf's 242 test lines
Koin and Navigation Compose 2.9 see D11

7. Internal drift

This is where most of the work is. Grouped by theme; each group lists the main examples, not every site.

ViewModels

State

Composables

Design system

Data layer

DI

Naming

Hygiene

Tests

8. Docs drift

Agents read these files on every task. Each wrong line here produces wrong code later.

Status: fixed by #1065 and #1076; the table stays as the record of what had drifted.

Claim Reality
Root CLAUDE.md: version 1.8.3 (code 18) the catalog says 1.9.3 / 22 (libs.versions.toml:59-60)
Feature list has 11 features, typo "homeP" 13 features; feed and repo-pages are missing (settings.gradle.kts:69-71, settings.gradle.kts:58-60)
favourites and starred are presentation-only their domain/data modules exist and are empty; profile and tweaks are presentation-only too and are not listed
Route list includes HomeScreen, OnboardingScreen neither exists; 13 real routes are missing (GithubStoreGraph.kt:8)
Core repositories include AuthenticationState, ThemesRepository, TelemetryRepository, SearchHistoryRepository the first is now UserSessionRepository, the next two don't exist, the last is in feature/search/domain (SearchHistoryRepository.kt:5)
HostTokenInterceptor injects per-host PATs it was deleted in #666 (bug 1)
KSafe wraps every persisted credential through safeGet / safePut raw calls exist, including the token store itself (HostTokenRepositoryImpl.kt:138)
Tech: Landscapist 2.9.5 images use Coil 3 (build.gradle.kts:15-17)
core/presentation is a "Material 3 theme" it is the custom Personality system; Material only receives colors (PersonalityThemeProvider.kt:39-47)
Every ViewModel follows the _state + _events + onAction template 18 of 33 do, counting stateIn as allowed; the rest vary (D1)
Packages are zed.rainxch.{module}.{layer} search domain breaks it (SearchRepository.kt:1)
feature/home: 7-day cache TTL, dedupe by fullName 1 hour (CachedRepositoriesDataSourceImpl.kt:480), dedupe by id (HomeViewModel.kt:264)
feature/details: injects TelemetryRepository, ProfileRepository; DownloadStage ends in DONE real deps differ (DetailsViewModel.kt:145); there is no DONE (DownloadStage.kt:3-9)
feature/tweaks: sections Account, Language, Network; Android restart works none of those sections exist; the Android restart is empty (RestartApp.android.kt:3-5)
feature/search: queries are debounced no debounce exists
feature/auth: no retry loops on top of HttpRequestRetry withRetry(3) wraps a client that already retries (GitHubAuthApi.kt:67)
CONTRIBUTING: CI lints every PR; MVI; wire ViewModels in initKoin.kt no PR CI; root CLAUDE.md says MVVM; ViewModels live in ViewModelsModule.kt

Recommendation: rewrite root CLAUDE.md to a short factual index, delete the stale parts of the feature files rather than patching every line, add the missing feed and repo-pages files, and point everything at one style guide.

9. Where each rule lives

The higher the rung, the less it depends on anyone remembering.

Rule Rung Status
Formatting, imports, naming basics Spotless (ktlint 1.8.0) blocking on files changed against main
Compose API basics (modifier, parameter order, naming) Spotless with the Compose rules same
Complexity, exceptions, smells detekt per-module baselines, new findings fail
Android Lint lintDebug per-module baselines
Compose stability stability analyzer committed stability/*.stability files
Common domain code imports no Ktor, Room, Compose, lifecycle, Koin or Android Konsist passing
Presentation never imports data Konsist passing; MirrorPickerViewModel is allowlisted for the core.data import that came with #1015
Packages under zed.rainxch.<owner>.<layer> Konsist passing, 7 search files allowlisted
ViewModels named *ViewModel and only in presentation or composeApp Konsist passing
Top-level *Action / *Event types are sealed Konsist passing
File size ratchet, 800 lines (D8) Konsist passing, 15 files allowlisted
XViewModel shape (D1) Konsist follow-up, with an allowlist of today's off-pattern ViewModels
No runCatching around suspend calls (A1), no e.message in presentation (A3), no MaterialTheme or Color(0x...) in features, no koinInject in composables Konsist follow-up, with allowlists
Previews (A5), CLAUDE.md changes with the code (A9) review
All of the above, plus compile and jvmTest with coverage, on every PR GitHub Actions pr-checks.yml in place
Everything else docs/code-style.md in place

10. Follow-up work

In order:

  1. Done in #1076. Contributor docs: a short public AGENTS.md, a slimmer public CLAUDE.md, a refreshed CONTRIBUTING.md, a PR template, and the feature CLAUDE.md files corrected against the code (already prepared).
  2. Open fix PRs land first, after #1056 and this change: #1063 (bug 1), #1062 (bug 2), the rate-limit fix (bugs 3 and 4), and other open work, so none of them has to rebase across a mass format.
  3. Done in #1080. The one-time format (D4) as its own PR at a quiet moment: one spotlessApply over every file with the ratchet off for that run, the Compose findings fixed or suppressed, and the feature:details:presentation warnings opt-out dropped with its five warnings fixed. Then the squash SHA goes into .git-blame-ignore-revs.
  4. Code changes from the decisions, one PR each:
    • power-assert (A6), no wildcard imports (A7) and no stability annotations on screen state (D9), right after the format
    • error handling (D2, A1, A3) with Konsist rules for runCatching around suspend calls and e.message in presentation
    • core UI: a shared load / error / empty renderer (A2), localized content descriptions (A4), previews (A5)
    • ViewModels and navigation: the D1 shape as a Konsist rule, navigation lambdas (D3), typed route arguments (A8)
    • the test stack and the first ViewModel tests (D10)
    • logging through the injected KomiStoreLogger (D7)
  5. Done. Issues for bugs 5 to 9: #1071 to #1075.

--- METRICS ---