Teaches AI agents to write idiomatic Kotlin (data classes, val, scope fns, Kotest) AND to make the right stack choices on JVM: Kotlin 2.3 + JDK 21 + Gradle Kotlin DSL, Ktor for HTTP, kotlinx-coroutines, DJL for ML inference, JavaCV for vision, Koog for AI agent orchestration.
95
95%
Does it follow best practices?
Impact
95%
1.23xAverage score across 10 eval scenarios
Passed
No known issues
Reviews an API surface — anything from a single public function to a published library — for the design concerns that the always-on idiom rules (use-data-class, nullable-question-mark, extension-over-util, …) don't cover. This guidance is here, not in always-on rules, because it's situational: it earns its keep when you're shaping an API, not on every line of code.
Process steps in order. Do not skip ahead. Step 0 selects which later steps apply: Step 1 runs only for a published / binary-stable surface and Step 8 only for a Kotlin Multiplatform library — the gates are not optional-by-default, Step 0 decides them. Steps 2–7, 9, and 10 always apply. Unless a step says otherwise, continue immediately to the next.
public declarations, @PublishedApi (inlined into consumer bytecode — treat as public), multiplatform expect/actual, and who depends on it (same module, other modules, external consumers)Skip for internal / application code. For a published, binary-stable surface, first know which kind of compatibility a finding breaks — name it in the report:
NoSuchMethodError). Hardest to preserve, most important for a published libraryFlag, per declaration:
explicitApi() in the Kotlin compiler options), which makes the compiler enforce explicit return types and visibility modifiers across the public surface$default method, whose signature carries the full parameter list plus a flags mask; add or reorder a parameter and that synthetic's signature shifts, so stale Kotlin client bytecode breaks. @JvmOverloads doesn't fix this — it generates Java-visible overloads that keep Java callers binary-compatible, but it leaves the Kotlin $default mechanism untouched, so Kotlin callers still break. Prefer explicit manual overloadsdata class in public API — generated constructor / copy() / componentN() signatures shift when a property is added or reordered. Prefer a regular class for stable surface (this is the public-API exception to the use-data-class rule)List → Collection) breaks callers that index; narrowing (Collection → List, Number → Int, Optional<T> → T?) keeps source compat but breaks binary compat. Route through a deprecation cycle, not an in-place swap (the public-API exception to nullable-question-mark)@PublishedApi declarations — internal members marked @PublishedApi get inlined into client bytecode (the mechanism that lets a public inline function call them, since inline public functions can't reference plain non-public declarations). They are effectively public — apply every rule above to them@Deprecated(message, replaceWith, level); communicate the versioning/deprecation policy to usersapiDump / apiCheck, commit the .api file; built into the Kotlin Gradle plugin 2.2.0+) if not already wired in@RequiresOptIn markers (Preview/Experimental/Delicate) for unstable surface, each category documented in KDoc; propagate the marker when the library itself consumes an experimental API from a dependency. Do NOT use opt-in to deprecate — that's @Deprecated's jobProceed immediately to Step 2.
List, Map, Duration, Result, Sequence) instead of inventing wrappers that consumers must learn and convert tosealed types over open when only specific implementations are valid; enables exhaustive when with no else, so a new subtype turns every unhandled branch into a compile errorList / Set / Map, never the live mutable internal; return .toList() when callers need a stable snapshot; keep Array out of public signatures (vararg + spread already defensively copies)require() for arguments (IllegalArgumentException), check() for instance state (IllegalStateException); put the offending value in the message, never sensitive dataflow.filter().map().buffer() beats one function with filter/map/buffer flagsextension-over-util rule)doWork(true) is unreadable at the call site; split into named functions (map / mapNotNull), use an enum for three-plus modes, or at least require the flag be passed as a named argument (overwrite = true)Int/Long/Double for arithmetic, Byte/Short/Float for storage constraints, unsigned types for full-positive/interop, inline value classes for IDs and other non-arithmetic entitieselement vs item vs entry — pick one)BigDecimal(200) == BigDecimal("200"))OrNull suffix → nullable return; Catching suffix → exception-wrappingResult<T>); don't use exceptions for normal control flowtoString() on every stateful type (including internal ones) — no sensitive data, consistent format; document the format only if it's part of the contract. (Note: don't reach for data class just to get toString() on public API — see Step 1)cause) when the dependency is an implementation detailsealed/interface seam, not a final concrete classProceed to Step 8 only if this is a Kotlin Multiplatform library; otherwise skip directly to Step 9.
Skip entirely for JVM-only libraries. When the library targets KMP:
commonMain first, then an intermediate set (concurrent, nonJvm), then a platform set (androidMain) only for genuinely platform-exclusive APIscommonMain without platform-specific glue; sensible defaults, platform options only where neededexpect/actual and document any unavoidable platform difference.klib for Apple targets without an Apple machine), tier Kotlin/Native targetskotlin-test for common tests plus the platform runnersProceed to Step 9.
@see, mirrored format/parse)@suppress KDoc tagProceed to Step 10.
file:line — <category>: <problem>. <fix>.require() a precondition) can be applied directly