{"id":50787958,"url":"https://github.com/vkrychun/stem-runtime-kotlin","last_synced_at":"2026-06-12T09:04:37.555Z","repository":{"id":362687063,"uuid":"1238601749","full_name":"vkrychun/stem-runtime-kotlin","owner":"vkrychun","description":"Runtime engine for StemJSON - declarative language for AI","archived":false,"fork":false,"pushed_at":"2026-06-05T12:17:22.000Z","size":2949,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T14:10:53.640Z","etag":null,"topics":["aar","ai","android","backend-driven-ui","declarative-ui","jetpack-compose","kotlin","sdk","server-driven-ui","stemjson"],"latest_commit_sha":null,"homepage":"https://stemjson.com","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vkrychun.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":".github/SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-14T09:16:36.000Z","updated_at":"2026-06-05T12:16:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vkrychun/stem-runtime-kotlin","commit_stats":null,"previous_names":["vkrychun/stem-runtime-kotlin"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/vkrychun/stem-runtime-kotlin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vkrychun%2Fstem-runtime-kotlin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vkrychun%2Fstem-runtime-kotlin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vkrychun%2Fstem-runtime-kotlin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vkrychun%2Fstem-runtime-kotlin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vkrychun","download_url":"https://codeload.github.com/vkrychun/stem-runtime-kotlin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vkrychun%2Fstem-runtime-kotlin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34236583,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aar","ai","android","backend-driven-ui","declarative-ui","jetpack-compose","kotlin","sdk","server-driven-ui","stemjson"],"created_at":"2026-06-12T09:04:36.516Z","updated_at":"2026-06-12T09:04:37.547Z","avatar_url":"https://github.com/vkrychun.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# StemRuntimeSDK\n\nYour AI can now ship complete native Android features, not just code snippets. **StemJSON** is a declarative language describing a full feature — screens, interactions, data, navigation — and **StemRuntimeSDK** runs it as native Jetpack Compose on-device. AI authors the feature; users get native Android.\n\n![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?logo=android)\n![Kotlin](https://img.shields.io/badge/Kotlin-2.0%2B-7F52FF?logo=kotlin)\n![Compose](https://img.shields.io/badge/Jetpack%20Compose-BoM%202024.12-4285F4?logo=jetpackcompose)\n![Gradle](https://img.shields.io/badge/Gradle-8.x-02303A?logo=gradle)\n![License](https://img.shields.io/badge/license-Proprietary%20Freeware-lightgrey)\n\n---\n\n## Table of Contents\n\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Zip-Packaged Modules](#zip-packaged-modules)\n- [Core API](#core-api)\n- [State Observation \u0026 Events](#state-observation--events)\n- [Module Lifecycle](#module-lifecycle)\n- [License \u0026 Watermark](#license--watermark)\n- [Navigation Embedding](#navigation-embedding)\n- [Custom Repositories](#custom-repositories)\n- [Custom Services](#custom-services)\n- [Error Handling](#error-handling)\n- [Diagnostics \u0026 Logging](#diagnostics--logging)\n- [Module JSON](#module-json)\n- [Coroutines \u0026 Threading](#coroutines--threading)\n- [Privacy \u0026 Security](#privacy--security)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Requirements\n\n| Dependency | Minimum |\n|---|---|\n| Android | 7.0 (API 24) |\n| compileSdk | 35 |\n| Kotlin | 2.0 |\n| Android Gradle Plugin | 8.0 |\n| Jetpack Compose | BoM 2024.12.01 |\n| Core-library desugaring | enabled (`desugar_jdk_libs` 2.0+) |\n\nDesugaring is required because the SDK uses `java.time.*` on `minSdk 24`. Without it, `:checkDebugAarMetadata` fails with a clear message.\n\n---\n\n## Installation\n\n### Maven repository\n\nAdd the Maven mirror to `settings.gradle.kts`:\n\n```kotlin\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url = uri(\"https://raw.githubusercontent.com/vkrychun/stem-runtime-kotlin/main\") }\n    }\n}\n```\n\n### App module\n\nIn your app module's `build.gradle.kts`:\n\n```kotlin\nplugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.kotlin.compose)\n}\n\nandroid {\n    compileSdk = 35\n    defaultConfig { minSdk = 24 }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n        isCoreLibraryDesugaringEnabled = true\n    }\n\n    buildFeatures { compose = true }\n}\n\ndependencies {\n    coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:2.1.4\")\n    implementation(\"com.stemjson:stem-runtime-sdk:1.0.2\")\n}\n```\n\nThe AAR's POM and Module Metadata bring the transitive Compose, Coil, and Media3 graph — no extra `implementation` lines needed.\n\n---\n\n## Quick Start\n\n```kotlin\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.platform.LocalContext\nimport com.stemjson.runtime.StemRender\nimport com.stemjson.runtime.StemRuntime\nimport com.stemjson.runtime.StemValidationOutcome\n\nclass DashboardActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent { MaterialTheme { Dashboard() } }\n    }\n}\n\n@Composable\nprivate fun Dashboard() {\n    val context = LocalContext.current\n    val runtime = remember { StemRuntime(context) }\n    var render by remember { mutableStateOf\u003cStemRender?\u003e(null) }\n\n    LaunchedEffect(Unit) {\n        val bytes = context.assets.open(\"dashboard.json\").use { it.readBytes() }\n        val outcome = runtime.validate(bytes)\n        if (outcome is StemValidationOutcome.Success) render = outcome.render\n    }\n\n    render?.Render() ?: CircularProgressIndicator()\n}\n```\n\nThree steps in practice: create a runtime, validate JSON bytes (single file or zip-packaged module), embed the returned render — `StemRender.Render()` is a `@Composable` function. The SDK accepts either a single `.json` file or a zip-packaged module and picks the loader from the byte stream — no flag required.\n\n---\n\n## Zip-Packaged Modules\n\nUse a zip when a module needs bundled assets, localisation, or sub-modules.\n\n```\nmy_feature.zip\n├── main.json                  ← required — the module root\n├── details.json               ← sub-module, loaded via file://details.json\n├── localization/\n│   ├── en.strings             ← \"key\" = \"value\"; format\n│   └── uk.strings\n└── assets/\n    └── logo.png               ← loaded via file://assets/logo.png\n```\n\n- Package resources are referenced with `file://\u003crelative-path\u003e` and take precedence over host-app resources with the same path.\n- A zip without `main.json` at the root fails validation.\n- `.strings` files under `localization/` back `l10n://` sources and the `localize(key, fallback)` expression function. The runtime falls back to the host app's resources if a key is missing.\n\nSee [StemJSON Specification §14](https://github.com/vkrychun/StemJSON/blob/main/spec/v1.0.md#14-package--distribution) for the full package format.\n\n---\n\n## Core API\n\n### `StemRuntime`\n\nThe entry point. Create one per app or feature scope.\n\n```kotlin\npublic class StemRuntime(\n    context: Context,\n    public val configuration: Diagnostics.Configuration = Diagnostics.Configuration(),\n)\n```\n\n```kotlin\n// Default — picks up the application context for you\nval runtime = StemRuntime(context)\n\n// With diagnostics\nval runtime = StemRuntime(\n    context,\n    Diagnostics.Configuration(enabled = true, minLevel = StemSeverity.WARNING),\n)\n```\n\nAll chainable configurators return `this`, so they compose fluently:\n\n```kotlin\nval runtime = StemRuntime(context)\n    .navigationEmbedded()\n    .license(\"…\")\n    .watermarkPosition(StemWatermarkPosition.TopEnd)\n```\n\nFor repository / service / picker registration, see the [Custom Repositories](#custom-repositories) and [Custom Services](#custom-services) sections.\n\n### Validation\n\n```kotlin\npublic suspend fun validate(\n    bytes: ByteArray,\n    ignore: Set\u003cStemSeverity\u003e = emptySet(),\n): StemValidationOutcome\n```\n\n`ignore` suppresses the listed severity levels from causing a `Failure` (e.g. `setOf(StemSeverity.WARNING, StemSeverity.NOTE)`).\n\n`StemValidationOutcome` is a sealed interface; the validation report is **present on both branches** — even a successful validation may carry advisory notes/warnings:\n\n```kotlin\npublic sealed interface StemValidationOutcome {\n    public val report: StemValidationReport\n\n    public data class Success(val render: StemRender, override val report: StemValidationReport) : StemValidationOutcome\n    public data class Failure(override val report: StemValidationReport) : StemValidationOutcome\n\n    public fun renderOrNull(): StemRender?\n    public fun onSuccess(block: (StemRender) -\u003e Unit): StemValidationOutcome\n    public fun onFailure(block: (StemValidationReport) -\u003e Unit): StemValidationOutcome\n}\n```\n\nTypical handling:\n\n```kotlin\nruntime.validate(bytes)\n    .onSuccess { render = it }\n    .onFailure { Log.w(\"Stem\", it.render()) }\n```\n\n`StemValidationReport.render()` is the formatted, human- and machine-readable form:\n\n```\n=== Validation Report: 2 errors, 1 warning ===\n[V002] [error] login_btn V002: Value 'repositoryId' is missing\n[N007] [warning] product_list N007: Endpoint returned 404 for products.json\n```\n\nEach line: `[code] [severity] \u003cpath\u003e \u003ccode\u003e: \u003cmessage\u003e`. The format is designed for **AI-in-the-loop authoring**: feed the report back to the model and it will revise the StemJSON module until validation passes.\n\n### `StemRender`\n\nThe renderable handle returned by `validate`. Embed it via its `@Composable Render()` function or read named context values through the indexed-access operator:\n\n```kotlin\n// 1. Embed in Compose\nrender.Render()\n\n// 2. Read named context values declared in the module's JSON `context`\nval title: String? = render[\"title\"]\nval icon:  String? = render[\"icon\"]\n```\n\n`render.Render()` is the only composition entry point. It runs the SDK integrity checks and wraps the module body with the unlicensed-build watermark on every composition — both happen unconditionally and cannot be skipped by host code.\n\nEach `StemRender` carries a stable `unitId` so it can be used as a key in `LaunchedEffect`, `DisposableEffect`, `remember(render.unitId) { … }`, and Compose recomposition.\n\n---\n\n## State Observation \u0026 Events\n\n### Subscribe to a state key (callback form)\n\n```kotlin\npublic fun subscribe(\n    key: String,\n    render: StemRender,\n    handler: (Any?) -\u003e Unit,\n): Closeable\n```\n\n```kotlin\nval handle: Closeable = runtime.subscribe(\"cartCount\", render) { value -\u003e\n    updateBadge(value as? Long ?: 0L)\n}\n// later\nhandle.close()\n```\n\n### Stream state changes (Flow form)\n\n```kotlin\npublic fun stream(key: String, render: StemRender): Flow\u003cAny?\u003e\n```\n\n```kotlin\nLaunchedEffect(render.unitId) {\n    runtime.stream(\"cartCount\", render).collect { value -\u003e\n        updateBadge(value as? Long ?: 0L)\n    }\n}\n```\n\nEmitted values are JSON-shaped — `null`, `String`, `Long`, `Double`, `Boolean`, `List\u003cAny?\u003e`, or `Map\u003cString, Any?\u003e`. Cast to your expected type inside the collector. The stream is `distinctUntilChanged`, so identical consecutive values do not re-emit.\n\n`runtime.subscribe` is preferable when the consumer is not already inside a coroutine scope; `runtime.stream` is preferable when composing with other flows or applying operators.\n\n### Trigger events from native code\n\n```kotlin\npublic fun trigger(event: String, data: Any)\n```\n\n```kotlin\nruntime.trigger(\"themeChanged\", mapOf(\"mode\" to \"dark\"))\n```\n\n`data` accepts any JSON-shaped value — a `Map\u003cString, Any?\u003e` payload is typical, but a primitive or list is also fine. The value is bound into the matching `onCustom` handler's context. Inside the module JSON, read fields as `@{\u003caction.id\u003e.\u003cfield\u003e}`. Always pass every field the handler needs in the payload — path predicates with `@{…}` are not supported inside filter values (see StemJSON spec §6.2.1).\n\n---\n\n## Module Lifecycle\n\nA module cannot terminate itself — it only mutates its own state. The host observes a sentinel state key and calls `kill`:\n\n```kotlin\npublic suspend fun kill(render: StemRender)\n```\n\n```kotlin\nLaunchedEffect(render.unitId) {\n    runtime.stream(\"onClose\", render).collect { value -\u003e\n        if (value == true) {\n            runtime.kill(render)\n            onDismiss()\n        }\n    }\n}\n```\n\n`kill` resets the rendered module to its initial JSON state. The render handle stays valid — composing it again shows the module fresh, with an empty navigation stack and the JSON-declared initial state. Inherited context, registered dependencies, named actions, packaged localisation, and the component palette are retained.\n\nTo fully release every runtime-held resource (cancel coroutines, drop every store), call `release()`:\n\n```kotlin\npublic fun release()\n```\n\nAfter `release()` the runtime instance is unusable; create a fresh `StemRuntime(context)` for a new feature scope.\n\n---\n\n## License \u0026 Watermark\n\nUnlicensed builds display a small \"Powered by StemJSON\" badge over every rendered module on physical devices. A valid license key suppresses the badge; the corner is configurable:\n\n```kotlin\nval runtime = StemRuntime(context)\n    .license(\"a1b2c3d4e5f6…\")                     // suppresses the badge\n    .watermarkPosition(StemWatermarkPosition.TopEnd) // moves it (no-op when licensed)\n```\n\n`StemWatermarkPosition` values: `BottomEnd` (default), `BottomStart`, `TopEnd`, `TopStart`. End/Start honour the device locale's layout direction.\n\nThe license key is issued for your app's `packageName`; an invalid or wrong-package key is dropped silently. Removing the badge requires a valid key — `watermarkPosition` only moves it.\n\n---\n\n## Navigation Embedding\n\nBy default a module creates its own navigation stack. When the module is pushed **inside a host navigation flow**, call `.navigationEmbedded()` so internal `navigation` components participate in the host's stack instead:\n\n```kotlin\nval runtime = StemRuntime(context).navigationEmbedded()\n```\n\nWith this enabled, `link` destinations and `navigate push` actions land on the host's stack; system back and pop operations sync automatically.\n\n\u003e **`link.destination` must have `\"type\": \"module\"`.** A `scroll` / `vstack` placed there renders but its `events` (notably `onAppear`) will not fire. Always wrap pushed layouts as `{ \"type\": \"module\", \"state\": {…}, \"children\": [ … ] }`. See StemJSON spec §link.\n\nDo **not** use `.navigationEmbedded()` for self-contained modules (tab root, modal presentation) — they manage their own navigation.\n\n---\n\n## Custom Repositories\n\nRegister custom factories via:\n\n```kotlin\npublic fun register(kind: StemDependencyKind, factory: StemDependencyFactory): StemRuntime\n```\n\nThe factory receives a `StemDependencyContext` (`id`, `category`, `kind`, JSON-shaped `config: Map\u003cString, Any?\u003e`) and returns a `StemDependable`.\n\nBuilt-in repositories, registered automatically and overrideable:\n\n| Key | Built-in implementation |\n|---|---|\n| `StemRepositoryType.REMOTE` | OkHttp HTTP/REST client |\n| `StemRepositoryType.LOCAL` | SQLite-backed document storage |\n| `StemRepositoryType.SECURED` | EncryptedSharedPreferences via Tink |\n| `StemRepositoryType.PHOTOS` | Android Photo Picker — `StemRender.Render()` auto-registers the launcher; no host wiring needed |\n\nThe `StemRepository` contract:\n\n```kotlin\npublic interface StemRepository : StemDependable {\n    public suspend fun read(params: Map\u003cString, Any?\u003e): Any?\n    public suspend fun create(params: Map\u003cString, Any?\u003e): Any?\n    public suspend fun update(params: Map\u003cString, Any?\u003e): Any?\n    public suspend fun delete(params: Map\u003cString, Any?\u003e): Any?\n}\n```\n\n`params` and return values are JSON-shaped (`String` / `Long` / `Double` / `Boolean` / `null` / `List\u003cAny?\u003e` / `Map\u003cString, Any?\u003e`). Throw `StemActionException(code, message)` to route to the action's `output.failure` chain — `code` and `message` get bound under `@{actionId.code}` / `@{actionId.message}`.\n\nImplement only the operations you support by extending `StemRepositoryAdapter` — unimplemented methods throw `unsupportedOperation` for you:\n\n```kotlin\nclass ProductRepository(id: String, private val baseUrl: String) : StemRepositoryAdapter(id) {\n    override suspend fun read(params: Map\u003cString, Any?\u003e): Any? = okHttp.get(\"$baseUrl/products\")\n}\n```\n\nRegistration follows the same shape as services — either override a built-in kind, or define your own `StemDependencyKind`:\n\n```kotlin\n// Override the built-in REMOTE repository for every module that declares `kind: \"remote\"`\nruntime.register(StemRepositoryType.REMOTE) { ctx -\u003e\n    ProductRepository(ctx.id, ctx.config[\"baseUrl\"] as String)\n}\n\n// Or define a new repository kind:\nenum class AppRepository(override val kind: String) : StemDependencyKind {\n    PRODUCTS(\"products\");\n\n    override val category: String = \"repository\"\n}\n\nruntime.register(AppRepository.PRODUCTS) { ctx -\u003e\n    ProductRepository(ctx.id, ctx.config[\"baseUrl\"] as String)\n}\n```\n\nThe matching JSON `dependencies` entry references it by `category` + `kind`:\n\n```json\n{\n  \"dependencies\": [\n    {\n      \"id\": \"productsApi\",\n      \"category\": \"repository\",\n      \"kind\": \"products\",\n      \"config\": { \"baseUrl\": \"https://example.com/v1\" }\n    }\n  ]\n}\n```\n\nFor streaming sources (WebSocket, Firestore listener, SSE), also implement `StemListenable` to back the spec's `listen` action. `listen(params)` returns a `Flow\u003cAny?\u003e` of JSON-shaped values:\n\n```kotlin\nclass TickerRepository(\n    override val id: String,\n    private val socket: WebSocket,\n) : StemRepositoryAdapter(id), StemListenable {\n    override fun listen(params: Map\u003cString, Any?\u003e): Flow\u003cAny?\u003e =\n        socket.observe(channel = params[\"channel\"] as String)\n}\n```\n\nThe dispatcher cancels the underlying coroutine when the listen action is cancelled or the module is killed, so cooperatively-cancellable `collect` is all the implementation needs.\n\n### Custom photo picker\n\nThe `photos` repository works out of the box — `StemRender.Render()` registers an Android Photo Picker launcher for the lifetime of every composition. Override it only to swap in an in-app gallery, a pre-curated set, or a test double. Implement `StemPhotoPicker.pick(selectionLimit, mediaTypes)` to return a list of URI strings:\n\n```kotlin\nclass CuratedGalleryPicker(private val library: List\u003cString\u003e) : StemPhotoPicker {\n    override suspend fun pick(\n        selectionLimit: Int,\n        mediaTypes: List\u003cString\u003e,\n    ): List\u003cString\u003e = library.take(selectionLimit)\n}\n\nruntime.setPhotoPicker(CuratedGalleryPicker(myUriList))\n```\n\n`setPhotoPicker` returns the runtime for fluent chaining and accepts `null` to clear a previously installed custom picker (the runtime falls back to the auto-registered default).\n\nWorked patterns live in [`stem-examples-kotlin`](https://github.com/vkrychun/stem-examples-kotlin).\n\n---\n\n## Custom Services\n\nServices handle operations outside CRUD semantics — analytics, biometrics, camera, deep links, health, and so on.\n\nBuilt-in services, registered automatically:\n\n| Key | Built-in implementation |\n|---|---|\n| `StemServiceType.AUDIO` | System sounds and haptics (uses `VIBRATE`) |\n| `StemServiceType.PUSH` | Local notifications (uses `POST_NOTIFICATIONS` on API 33+) |\n| `StemServiceType.LOCATION` | `FusedLocationProviderClient` — requires `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION` host-side |\n\nThe `StemService` contract:\n\n```kotlin\npublic interface StemService : StemDependable {\n    public suspend fun execute(input: Any?): Any?\n}\n```\n\n`input` is the resolved JSON-shaped payload from the action — typically a `Map\u003cString, Any?\u003e`, sometimes a primitive. Throw `StemActionException` to trigger `output.failure`.\n\n```kotlin\nclass AnalyticsService(override val id: String) : StemService {\n    override suspend fun execute(input: Any?): Any? {\n        analytics.track(input as Map\u003cString, Any?\u003e)\n        return null  // fire-and-forget — return a Map / List / primitive for output.success\n    }\n}\n```\n\nDefine a new dependency kind for services the SDK doesn't ship as a built-in, then register your factory against it:\n\n```kotlin\nenum class AppService(override val kind: String) : StemDependencyKind {\n    ANALYTICS(\"analytics\"),\n    BIOMETRICS(\"biometrics\");\n\n    override val category: String = \"service\"\n}\n\nruntime.register(AppService.ANALYTICS) { ctx -\u003e\n    AnalyticsService(ctx.id)\n}\n```\n\nThe matching JSON `dependencies` entry uses the `category` + `kind` discriminator:\n\n```json\n{\n  \"dependencies\": [\n    { \"id\": \"appAnalytics\", \"category\": \"service\", \"kind\": \"analytics\" }\n  ]\n}\n```\n\nTo replace a built-in (a host with its own audio engine, a test double for push, etc.), pass the built-in `StemServiceType` to `register` instead of your custom enum — the rest of the call is identical.\n\nWorked patterns live in [`stem-examples-kotlin`](https://github.com/vkrychun/stem-examples-kotlin).\n\n---\n\n## Error Handling\n\nValidation surfaces a `StemValidationReport`; runtime failures inside repositories and services surface as `StemActionException`.\n\n```kotlin\nwhen (val outcome = runtime.validate(bytes)) {\n    is StemValidationOutcome.Success -\u003e render = outcome.render\n    is StemValidationOutcome.Failure -\u003e Log.e(\"Stem\", outcome.report.render())\n}\n```\n\n`StemActionException`:\n\n```kotlin\npublic class StemActionException(public val code: String, message: String) : RuntimeException(message)\n```\n\nThrow it from any `StemRepository` / `StemService` / `StemListenable` implementation to route the action chain to its `output.failure` handler. The action binds `@{actionId.code}` and `@{actionId.message}` from the exception. Code values follow the catalogue in StemJSON spec §15 — e.g. `notAuthenticated`, `notFound`, `noConnection`, `keychainAccess`. Any other thrown exception is treated as an unknown failure with `code = \"unknown\"` and the exception's message.\n\n`StemIssueCode` (returned in `StemIssue` entries inside the report) is the validator-side catalogue: `V###` for structural issues, `E###` for general, `N###` for network, `S###` for storage, `K###` for security. The code string is stable across releases.\n\n---\n\n## Diagnostics \u0026 Logging\n\n```kotlin\npublic object Diagnostics {\n    public data class Configuration(\n        val enabled: Boolean = true,\n        val minLevel: StemSeverity = StemSeverity.NOTE,\n    )\n}\n```\n\n```kotlin\n// Default — enabled, minLevel = NOTE\nval runtime = StemRuntime(context)\n\n// Quieter — drop notes and below\nval runtime = StemRuntime(\n    context,\n    Diagnostics.Configuration(minLevel = StemSeverity.WARNING),\n)\n\n// Silence\nval runtime = StemRuntime(\n    context,\n    Diagnostics.Configuration(enabled = false),\n)\n```\n\nSeverity ladder (`StemSeverity`): `BINGO`, `INFO`, `NOTE`, `WARNING`, `ERROR`, `CRITICAL`. `ERROR` and `CRITICAL` block rendering.\n\nMessages are emitted through `android.util.Log` under the tag `com.stem.runtime.sdk`. Filter with:\n\n```bash\nadb logcat -s com.stem.runtime.sdk\n```\n\nThe runtime also routes every validation finding through the logger, so a host that never inspects the `StemValidationReport` still sees the validator's work in logcat.\n\n---\n\n## Module JSON\n\nStemJSON modules are a declarative tree: every component has a `type`, optional `context`, optional `state`, and optional `children`. Values anywhere in the tree may be static, state-bound (`${field}`), context-bound (`@{key}`), or expression-evaluated (`{{ expr }}`).\n\n```json\n{ \"id\": \"email_field\", \"type\": \"textfield\",\n  \"context\": { \"_label\": \"Email\", \"_text\": \"${email}\" } }\n```\n\nFor the full component catalogue, value syntax, style options, and action types see the [**StemJSON v1.0 Specification**](https://github.com/vkrychun/StemJSON/blob/main/spec/v1.0.md).\n\n---\n\n## Coroutines \u0026 Threading\n\nThe runtime's internal scope uses `Dispatchers.Main.immediate` for its supervisor job. State emissions, the custom-event bus, and action dispatch hop through that scope.\n\n`suspend` entry points:\n- `runtime.validate(bytes, ignore)`\n- `runtime.kill(render)`\n- `StemRepository.read` / `.create` / `.update` / `.delete`\n- `StemService.execute(input)`\n- `StemListenable.listen(params)` returns a `Flow\u003cAny?\u003e`\n\nRepository and service implementations are invoked from the main dispatcher — long-running work should be moved off-main internally with `withContext(Dispatchers.IO) { … }` so the UI stays responsive.\n\n`runtime.stream(key, render)` returns a cold `Flow\u003cAny?\u003e` whose values are JSON-shaped Kotlin primitives, `List\u003cAny?\u003e`, or `Map\u003cString, Any?\u003e`. Cast to your expected type inside the collector. The flow is `distinctUntilChanged`, so identical consecutive values do not re-emit.\n\n---\n\n## Privacy \u0026 Security\n\nStemRuntimeSDK runs entirely on-device. It contains no telemetry, no analytics, and no phone-home behaviour. The SDK transmits no data to Licensor.\n\nThe SDK's `AndroidManifest.xml` declares only two permissions, both for documented runtime features:\n\n| Permission | Why |\n|---|---|\n| `android.permission.VIBRATE` | `audio` service haptic playback |\n| `android.permission.POST_NOTIFICATIONS` | `push` service local notifications (API 33+) |\n\nFor your Application, remember to:\n\n1. Add any extra permissions your StemJSON modules cause — for example `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION` if you use the built-in `location` service.\n2. Drive the runtime-permission flow for sensitive permissions (`POST_NOTIFICATIONS`, location, photos). The SDK reads `ContextCompat.checkSelfPermission` at action-fire time, so any grant takes effect on the next action invocation.\n3. Disclose data flows your StemJSON modules cause (Keychain-equivalent secured storage, network requests, etc.) in your Play Store data-safety form.\n\nTo report a security vulnerability, see [`SECURITY.md`](.github/SECURITY.md).\n\n---\n\n## Contributing\n\nThis repository ships StemRuntimeSDK as a pre-compiled AAR. SDK source code is proprietary and is not published here. Bug reports, documentation fixes, and security disclosures are welcome — see [`CONTRIBUTING.md`](.github/CONTRIBUTING.md) and [`SECURITY.md`](.github/SECURITY.md).\n\nThe StemJSON data format was originated and authored by **Vasyl Krychun** and is published separately under the Open Web Foundation Agreement 1.0 at [`github.com/vkrychun/StemJSON`](https://github.com/vkrychun/StemJSON).\n\n---\n\n## License\n\nDistributed under a Proprietary Freeware License. Unlicensed builds display a small \"Powered by StemJSON\" badge on physical devices.\n\nSee [`LICENSE`](LICENSE) for the EULA and [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md) for the attribution of the incorporated open-source components. The StemJSON format itself — originated and authored by Vasyl Krychun — is governed by the OWFa 1.0; see the [StemJSON spec repo](https://github.com/vkrychun/StemJSON).\n\nPricing: [stemjson.com/stemruntime#pricing](https://stemjson.com/stemruntime#pricing). Commercial enquiries: [vkrychun@stemjson.com](mailto:vkrychun@stemjson.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvkrychun%2Fstem-runtime-kotlin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvkrychun%2Fstem-runtime-kotlin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvkrychun%2Fstem-runtime-kotlin/lists"}