# GUIDE — Kotlin under the Discipline, v0.1

[p01] **Status.** Beta; seventh language. One document by the owner's structure: **Part I is the trunk** (core Kotlin), **Parts II–IV are inline area overlays** that inherit it and rewire only named sections. The trunk is sovereign — Kotlin Multiplatform makes the JVM just one target, so this is *not* an overlay on the Java trunk; instead, Part II **composes** with the Java ecosystem overlays (Spring) at the JVM, and Part IV defines the Discipline's first **inter-trunk boundary** (Kotlin/JS ↔ `GUIDE-TYPESCRIPT`). Sections stay isomorphic to the other guides.

[p02] Framing note — the seventh point of the typology. Rust *enforces*; TS *permits but compiles*; Python *trusts*; C++ *demands a subset*; Go *prescribes*; Java *bifurcates*; **Kotlin inverts the defaults**: everything Java left dangerous-by-default is opt-in here — null, mutability, inheritance (`final` by default), and in Multiplatform even platform access. Where the defaults already match the axioms, this guide is thin, like Go's. The gaps it closes: the **vanished failure surface** (checked exceptions removed and replaced with nothing at the signature level); the **GlobalScope escape hatch** in an otherwise natively-structured concurrency model; the **DSL/operator culture** (the language's superpower is also its R-021 risk zone); and **platform types** at the Java border. Patterns dissolve far: Strategy = interface + registry; Visitor = sealed + exhaustive `when`; Observer = `Flow` seam; Singleton = a *keyword* (`object`) — first-class syntax for the forbidden thing, governed below.

[p03] **Scope honesty.** Long-lived system/product code. Gradle build scripts (themselves Kotlin DSL) are tooling, not cells. Code generated by KSP processors follows the generated-code rule: processor *input* is the taggable unit; processors generating domain logic are infra-only.

## Part I — Core Kotlin (trunk)

### 0. Language baseline {#baseline}

- [p04] **Kotlin 2.1+ (K2 compiler)**, target latest stable. **Gradle** with version catalogs + convention plugins — the ecosystem reality (KMP is Gradle-only); Maven legal for pure-JVM targets. Dependency locking on (A2).
- **`allWarningsAsErrors = true`. `explicitApi()` is MUST for cell modules** — explicit visibility and return types on every public declaration: surface honesty as a compiler flag, not a review note.
- **Surface ratchet:** kotlinx-binary-compatibility-validator (Apache-2.0) — committed `.api` dumps, diffed in PRs; an unexplained surface change is a finding before it is an API break.
- **Lint gate:** detekt (Apache-2.0) — and its **baseline file is a native brownfield ratchet** (BROWNFIELD §B1 shipped inside the tool: existing findings frozen, new ones fail, the file only shrinks). Formatting via one pinned formatter (ktlint/ktfmt) — zero discipline budget.
- **Suppression policy (xfail-strict posture):** `@Suppress` only with the narrowest scope, the specific check name, and a `// reason`; a conform sweep re-runs named checks against suppressed elements and fails stale entries.
- **Null discipline:** `!!` is banned in cells (the assert-shaped escape hatch); `lateinit` is banned in cells (two-phase-init smell — constructor injection removes the need); **platform types (`T!`) are quarantined**: every value arriving from Java interop is pinned to explicit nullability at the boundary before it travels. `val` is the default; `var` is justified.
- **Tests:** kotlin-test in common code; JUnit 5 on JVM (EPL-2.0 flag inherited from the Java trunk); property testing via **kotest-property (Apache-2.0)** — a license upgrade over Java's EPL-flagged jqwik. **MockK and Mockito are banned in cell tests** — same rule, same reason as every sibling guide: capability injection makes module-graph mocking unnecessary.

### 1. Cells {#cells}

[p05] A cell is a Gradle module (in KMP, its `commonMain` source set — Part III makes that physics). `explicitApi` + the `.api` dump define its surface; promotion/extraction on the usual triggers.

- [p06] **Import-is-execution, Kotlin edition:** `object` and `companion object` initialize lazily on first touch; top-level property initializers run at file-class init. Side effects there are banned in cells. `object` is legal **only as a pure namespace** (stateless `val`s and functions); an `object` holding mutable state or doing registration is the singleton the trunk forbids, now with keyword support.
- **Capabilities are injected**, including the ones Kotlin is famous for forgetting: **`CoroutineDispatcher`/`CoroutineContext` is an injected capability** — hardcoded `Dispatchers.IO`/`Dispatchers.Main` in a cell is a finding (and the reason cell tests never need a main-dispatcher rule); `Clock` (kotlinx-datetime) injected; filesystem/network/env never ambient.
- Messages crossing seams are `data class`es of `val`s (or `data object`s); collections crossing seams are read-only types honestly backed.

[p07] Cell manifest (annotation carrier, §5):

[p08]
```kotlin
@SpecImplements("spec://org.vibevm.core/vibevm/modules/vibe-resolver/PROP-003#solver-upgrade", r = 2)
@Cell(seam = "DepSolver", variant = "sat", replaces = "naive", flag = "solver")
class SatDepSolver(
    private val provider: DepProvider,
    private val clock: Clock,
    private val dispatcher: CoroutineDispatcher,
) : DepSolver { /* ... */ }
```

### 2. Seams {#seams}

- [p09] A seam is an `interface` in the seams module; closed sets are `sealed`. Conformance is nominal — no assertions needed.
- **`suspend` functions are the async seam form.** This is a declared divergence from the Java trunk (which prescribes blocking seams on virtual threads): each trunk follows its platform's structured-concurrency culture, and at a mixed JVM boundary a one-function bridge (`runBlocking`/`Dispatchers` adapter) converts — the divergence is owned, not hidden.
- Interface default implementations follow the trunk-wide rule: trivial combinators only, never business flow (R-021).
- `Flow` at seams is **cold**; hot state (`StateFlow`/`SharedFlow`) is owned by a named state holder with an explicit lifetime, exposed read-only.

### 3. Registry and flags {#flags}

[p10]
```kotlin
// registry module — the only flag reader and the only module depending on cells.
fun depSolver(cfg: AppConfig, p: DepProvider, clock: Clock, io: CoroutineDispatcher): DepSolver =
    when (cfg.solver) {                       // provenance: default | env | cli | lockfile
        Solver.SAT   -> SatDepSolver(p, clock, io)
        Solver.NAIVE -> NaiveDepSolver(p)
    }
```

- [p11] **Tiers:** assembly tier = which modules (and, in KMP, which *targets*) are built — "is the code in the artifact"; runtime tier = a config data class bound once in `main`. **`expect`/`actual` never encodes product variants** — it is platform adaptation only; a product flag wearing `expect` clothing is a finding.
- No `ServiceLoader`-style discovery, no reflection wiring, no DI container in the trunk; Parts II–III legalize containers strictly as composition-root machinery.

### 4. Errors as contract {#errors}

[p12] Kotlin removed Java's checked exceptions — deliberately — and put nothing in the signature where they stood: exceptions here are unchecked *and* undeclared, the most vanished failure surface in the set. The Discipline restores it the same way as everywhere:

- [p13] **Seam-owned sealed results**, consumed by exhaustive `when` **as an expression, with no `else` branch** — a new permitted subtype must break every consumer's build (the default-arm ban, fourth appearance):

[p14]
```kotlin
sealed interface SolveResult {
    data class Ok(val resolution: Resolution) : SolveResult
    data class Err(val error: SolveError) : SolveResult
}
data class SolveError(val code: Code, val spec: String, val cause: Throwable? = null) {
    enum class Code { CYCLE, UNSATISFIABLE }
}
```

[p15] `spec` carries the violated REQ URI (PROP-014 §2.6). `kotlin.Result<T>` is **not** a seam type — its error channel is pinned to `Throwable`, which is an implementation genus, not a domain contract. Arrow's `Either`/`Raise` (Apache-2.0) is parked exactly as Effect-TS was: an optional profile for FP-shaped carriers, not the trunk binding.

- [p16] **Exceptions = invariant violations** (panic analog). Boundary adapters translate library exceptions into seam errors.
- **Cancellation sanctity — the №1 coroutine footgun:** a broad `catch (e: Exception)` silently eats `CancellationException` and breaks structured cancellation; every broad catch MUST rethrow it (detekt rule + conform check).
- **Structured concurrency is native — keep it that way:** `GlobalScope` is banned (the language's one escape hatch from its own best idea); `runBlocking` only at `main`/test edges; scopes are owned (`coroutineScope`/`supervisorScope` or an injected scope with a named lifetime); a coroutine that outlives its cell is a leak by definition.

### 5. specmark carrier {#specmark}

[p17] The same specmark **annotations** as Java, defined once in `commonMain` and visible to every target: `AnnotationRetention.BINARY` (in the artifact for analysis, invisible to runtime reflection), `@Repeatable`, **`@MustBeDocumented`** — edges render in Dokka, so the carrier doubles as the doc surface. File-level scope uses Kotlin's native syntax — `@file:SpecScope("<uri>", r = N)` — because the *file*, not the package, is Kotlin's organizational unit (no `package-info` needed). ≤3 edges per item or split.

### 6. Naming (R-020/R-021 bindings) {#naming}

- [p18] Computed `{Variant}{Seam}` → `SatDepSolver`; linted against the manifest.
- **The Kotlin theater list — placement, not amputation:** the language's DSL powers (lambda-with-receiver builders, `infix`, operator overloading) are *banned in domain cells* and *legal where they shine* — boundaries, configuration, test fixtures (Gradle and Ktor routing are the canonical homes). Also banned in cells: operator overloading beyond value semantics; delegated properties with side effects (`by lazy` of a pure value is fine; `observable`/`vetoable` is hidden control flow); extension functions on foreign types sprayed outside the owning cell; `kotlin-reflect`; companion-object registries; `!!`/`lateinit` (§0).

### 7. Replacement protocol (R-040 binding) {#replacement}

[p19] Differential oracle via kotest-property driving both cells through the seam (`@SpecVerifies`-tagged), divergences documented. Goldens follow the promotion protocol. On JVM the Java trunk's strict `@KnownFailing` JUnit extension carries over as the in-source twin of `tests-baseline.json`; in common tests, the baseline registry carries the weight alone (Go's situation) — stated, not hidden.

### 8. Risk table — trunk {#risks}

[p20]
| Footgun | Rule | Tier |
| --- | --- | --- |
| side effects in `object`/`companion`/top-level initializers | §1 | T-syn |
| mutable state in an `object`; companion registry | §1, §6 | T-syn |
| hardcoded `Dispatchers.*` in a cell | §1 | T-syn |
| `GlobalScope`; `runBlocking` outside main/tests; scope-leaking coroutine | §4 | T-syn |
| broad catch swallowing `CancellationException` | §4 | T-syn (detekt) |
| `else` branch on a sealed `when`; statement-`when` over sealed | §4 | T-syn |
| `kotlin.Result` in a seam signature | §4 | T-syn |
| `!!` / `lateinit` in a cell; unpinned platform type crossing inward | §0 | T-syn + T-sem |
| DSL builder / operator magic / side-effectful delegate in domain code | §6 | T-syn |
| `kotlin-reflect` in a cell | §6 | T-lex |
| MockK/Mockito in a cell test | §0 | T-syn |
| `expect`/`actual` encoding a product variant | §3 | review + T-syn |
| missing/changed `.api` dump without rationale | §0 | build diff |
| flag read outside the registry | R-001 | T-syn |
| public export without own/inherited spec edge | PROP-014 §3.2-6 | T-syn + index |

### 9. Doc layer {#docs}

[p21] KDoc + Dokka on every public declaration (`explicitApi` already forces you to notice them): behavior, seam error codes with REQ URIs, nullability beyond the types, coroutine context expectations (which dispatcher, cancellation behavior), `Flow` temperature. `@MustBeDocumented` specmark edges render alongside. Spec stays thin.

## Part II — Server & Desktop (area overlay)

[p22] **Inherits the trunk; JVM target composes with the Java ecosystem.**

- [p23] **ArchUnit rules apply verbatim** — it reads bytecode and does not care that the source was Kotlin; the ring rules ("cells import no framework") transfer without translation. Error Prone does not apply (javac-only); detekt carries the lint weight.
- **Spring composition:** `GUIDE-JAVA-SPRING` applies, with Kotlin clauses: the `kotlin-spring`/all-open compiler plugin is **scoped to boundary stereotypes only** — cells stay `final` (final-by-default is a Discipline ally; opening a cell for proxying is a finding). Constructor injection is the language's natural mode; `@ConfigurationProperties` binds to data classes. The concurrency divergence resolves per carrier, documented once: suspend seams with coroutine-aware boundary (WebFlux/MVC-with-suspend) *or* the Java trunk's Loom stance with bridges — one model per repository.
- **Ktor (Apache-2.0)** is the Kotlin-native boundary framework; its routing DSL is the placement rule in action — DSL at the boundary, plain calls inward.
- **Persistence:** Exposed/SQLDelight-class tools live at the boundary; SQLDelight's model (typed APIs generated *from SQL*) fits the generated-code rule cleanly — the `.sq` files are the taggable inputs.
- **Desktop:** Compose for Desktop is the JVM target of Part III's Compose rules — same state-hoisting discipline, same effect rules; packaging via jpackage.

[p24]
| Additional risks | Rule | Tier |
| --- | --- | --- |
| all-open applied to cell classes | II | build config audit |
| Spring/Ktor type in a seam signature | II | ArchUnit |
| `runBlocking` inside request handling | II | T-syn |

## Part III — Kotlin Multiplatform: Android & iOS, Compose (area overlay)

[p25] **The headline: `commonMain` is the cell ring made physics.** Common code *cannot name* a platform API — no `android.*`, no UIKit, no DOM exist in its classpath; the capability-injection rule every other guide enforces with linters is enforced here by the compiler's world model. **Cells live in `commonMain`**; `androidMain`/`iosMain` are boundary adapters.

- [p26] **`expect`/`actual` is the platform-capability seam** — narrow declarations (clock, storage, secure random), actuals are adapters. Business seams remain ordinary interfaces in common code. (Product variants never wear `expect` — trunk §3.)
- **iOS:** modern Kotlin/Native memory model assumed (no freeze-era folklore). The exported surface is an XCFramework governed by `explicitApi` + the `.api` dump; Swift/ObjC interop is boundary work, `suspend` crosses to Swift only through generated bridges/completion adapters at the boundary, never raw.
- **Android:** `Context` is the platform god-object and **physically cannot enter commonMain** — the architecture problem Android spent a decade on, dissolved by source-set geometry. Lifecycle, permissions, services: boundary. **DI:** Hilt is compile-time codegen (A3-friendlier than runtime containers) and is confined to the boundary ring; Koin (runtime locator) likewise if a carrier insists; cells are `new`-constructed inside producers either way.
- **Compose — the framework that agrees:** Compose's own law is that *composition must be side-effect-free* — the import-is-execution instinct, adopted by a UI runtime. Bindings: **state hoisting is capability injection for UI** — stateless composables take state and event lambdas; business logic never lives in a composable; side effects only in `LaunchedEffect`/`DisposableEffect` with honest keys; **`CompositionLocal` is the `http.DefaultClient` of Compose** — banned for domain dependencies, legal for theme/density as designed; `remember { }` holds pure values. State holders/ViewModels are boundary orchestration talking to seams. UI models crossing into composition are stable by construction (data classes of `val`s; `@Immutable` where inference needs help). Screenshot tests are goldens under the promotion protocol.

[p27]
| Additional risks | Rule | Tier |
| --- | --- | --- |
| platform API named in commonMain | III | compiler |
| domain dependency via `CompositionLocal` | III | T-syn |
| side effect in composition outside effect handlers | III | T-syn (compose lints) |
| business logic in a composable; stateful composable where hoisting fits | III | review + T-syn |
| `GlobalScope`/unowned scope in a ViewModel | trunk §4 | T-syn |
| raw `suspend` exported to Swift | III | export audit |

## Part IV — Kotlin/JS (area overlay)

[p28] **The Discipline's first inter-trunk boundary.** A Kotlin/JS artifact consumed by TypeScript code is a border between two guides of the same Discipline — `GUIDE-KOTLIN` on one side, `GUIDE-TYPESCRIPT` on the other. The contract is explicit:

- [p29] **`@JsExport` is the seam surface** — minimal, deliberate, and the **generated `.d.ts` is committed and diffed**: a cross-language surface ratchet, the `.api` dump's twin. TS-side conform treats the artifact as a typed boundary module; Kotlin-side conform audits the export set.
- Exported signatures avoid the classic traps: no `Long` across the border (JS numbers), no raw `suspend` (Promise adapters at the boundary), collections crossed as arrays/readonly shapes.
- **`dynamic` is banned in cells** — it is Kotlin's `any`, quarantined to boundary interop; `external` declarations are the typed border to JS libraries (the inward-facing `.d.ts`).
- npm dependencies are locked (`kotlin-js-store` committed — A2); IR backend + ES modules assumed.
- **Release map, double hop:** Kotlin/JS emits source maps, so the TS guide's chain extends — *minified frame → JS → Kotlin source → item → REQ*; ship/retain both map layers keyed by build id. Kotlin/Wasm: not baseline yet; named, not used.

[p30]
| Additional risks | Rule | Tier |
| --- | --- | --- |
| `dynamic` in a cell | IV | T-syn |
| `Long`/raw `suspend`/non-exportable type in `@JsExport` surface | IV | compiler + export audit |
| uncommitted lockfile or `.d.ts` drift | IV | build diff |

[p31] **First carrier note.** No Kotlin exists in vibevm; no carrier is designated. Trunk and areas ship genre-complete and unexercised under the carrier-relative house clause: rules remain DRAFT until a first Kotlin carrier exists, and at its first milestone any rule without a conform check (or explicit `WISH` mark) is removed rather than carried as aspiration.

