ketoy-dev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +101 -0
  3. package/SECURITY.md +34 -0
  4. package/dist/ketoy.js +3165 -0
  5. package/dist/ketoy.js.map +1 -0
  6. package/package.json +78 -0
  7. package/skills/ketoy/README.md +50 -0
  8. package/skills/ketoy/SKILL.md +148 -0
  9. package/skills/ketoy/examples/capabilities-stubs.kt +60 -0
  10. package/skills/ketoy/examples/hilt-config.kt +192 -0
  11. package/skills/ketoy/examples/no-hilt-config.kt +101 -0
  12. package/skills/ketoy/examples/todo-screen.kt +156 -0
  13. package/skills/ketoy/guides/build-and-analyze.md +87 -0
  14. package/skills/ketoy/guides/diagnose-errors.md +129 -0
  15. package/skills/ketoy/guides/init-project.md +127 -0
  16. package/skills/ketoy/guides/migrate.md +190 -0
  17. package/skills/ketoy/guides/publish-deferred.md +46 -0
  18. package/skills/ketoy/guides/safe-edits.md +141 -0
  19. package/skills/ketoy/reference/architecture-cheatsheet.md +122 -0
  20. package/skills/ketoy/reference/capabilities.md +122 -0
  21. package/skills/ketoy/reference/forbidden-apis.md +149 -0
  22. package/skills/ketoy/reference/supported-composables.md +80 -0
  23. package/skills/ketoy/reference/supported-constructors.md +57 -0
  24. package/skills/ketoy/reference/supported-modifiers.md +76 -0
  25. package/skills/ketoy/templates/app-build.gradle.kts.tmpl +109 -0
  26. package/skills/ketoy/templates/ketoy-capabilities.json.tmpl +21 -0
  27. package/skills/ketoy/templates/manifest-snippet.xml.tmpl +33 -0
  28. package/templates/HelloKetoyScreen.kt.tmpl +51 -0
  29. package/templates/MainActivity.kt.tmpl +53 -0
  30. package/templates/MyApplication.kt.tmpl +88 -0
  31. package/templates/ketoy-capabilities.json.tmpl +5 -0
@@ -0,0 +1,190 @@
1
+ # Migration Playbook — Compose → KBC
2
+
3
+ Migration is **always per-file, one-shot**. Never offer "migrate the whole project." Refuse if asked.
4
+
5
+ ## The audit pass (before writing any code)
6
+
7
+ For the file the user wants migrated, produce a written audit covering:
8
+
9
+ ### 1. Composables called
10
+
11
+ List every `Foo(...)` call. For each, check `reference/supported-composables.md`. Categorize:
12
+ - ✅ supported — fine
13
+ - ⚠️ supported via different overload — note the param list to use
14
+ - ❌ unsupported — propose alternative or recommend keeping native
15
+
16
+ ### 2. State holders
17
+
18
+ Find:
19
+ - `var x by remember { mutableStateOf(...) }` → migrate to `vmGetState` / `vmSetState`
20
+ - `collectAsState()` → migrate to `vmObserveState(...).collectAsState()` (collectAsState is supported via FLOW capability)
21
+ - `viewModel()` injection → migrate state slots to `vmGetState` + dispatch events via `VM_DISPATCH`
22
+ - `rememberSaveable` → migrate to `vmGetState` (KetoyVirtualViewModel persists to SavedStateHandle automatically)
23
+
24
+ ### 3. Side effects
25
+
26
+ Find every:
27
+ - `LaunchedEffect(...) { ... }` — keep as-is, but only call capability functions inside
28
+ - `DisposableEffect` — NOT supported. Rewrite using state observation or move to host.
29
+ - `SideEffect` — partial support (see KNOWN_ISSUES.md). Avoid in migrations.
30
+ - `coroutineScope { ... }` / `withContext(...)` — supported, but every suspend call must be a capability
31
+
32
+ ### 4. Forbidden API references
33
+
34
+ `grep` the file for:
35
+ - `android.*`, `androidx.*` outside Compose
36
+ - `kotlin.reflect`
37
+ - `java.io`, `java.nio`
38
+ - `GlobalScope`, `runBlocking`
39
+
40
+ For each, propose a capability replacement.
41
+
42
+ ### 5. Custom components
43
+
44
+ Any non-Compose-stdlib type construction. If it's a data holder, see if it can be passed via state (`Map<String, Any?>`). If it's a custom drawing primitive, recommend wrapping it as a custom composable adapter (out of migration scope) or keep native.
45
+
46
+ ## The migration step
47
+
48
+ 1. **Show the audit** to the user as a checklist. Get explicit acknowledgment of the migration scope.
49
+ 2. **Add or update capability stubs** in `Capabilities.kt` for every side effect / state observation.
50
+ 3. **Update `ketoy-capabilities.json`** with the new capabilities.
51
+ 4. **Update the host-side `CapabilityRegistry`** (either `AppCapabilityProvider.buildRegistry()` for Hilt or inline in `App.onCreate()` for non-Hilt). Each new capability needs its `register*(id) { args -> ... }` line.
52
+ 5. **Rewrite the composable**:
53
+ - Add `@KetoyEntryPoint @KetoyComposable` to the screen root.
54
+ - Replace state holders with `vmGetState` / `vmSetState`.
55
+ - Replace effects with capability calls.
56
+ - Replace forbidden API calls with capability calls.
57
+ - Hoist captured `val`s to the top of the function body for clarity.
58
+ 6. **Run `./gradlew :app:ketoyBundle --rerun-tasks`** — fix any compile errors via `guides/diagnose-errors.md`.
59
+ 7. **Run `./gradlew :app:assembleDebug`** — verify the bundle assembles into the APK.
60
+ 8. **Show user a diff** of every modified file before committing.
61
+
62
+ ## Migration patterns
63
+
64
+ ### State migration
65
+
66
+ **Before**:
67
+ ```kotlin
68
+ @Composable
69
+ fun Counter() {
70
+ var count by remember { mutableStateOf(0) }
71
+ Button(onClick = { count++ }) {
72
+ Text("Count: $count")
73
+ }
74
+ }
75
+ ```
76
+
77
+ **After**:
78
+ ```kotlin
79
+ @KetoyComposable
80
+ @Composable
81
+ fun Counter() {
82
+ val count = vmGetState("count") as Int? ?: 0
83
+ Button(onClick = { vmSetState("count", count + 1) }) {
84
+ Text("Count: $count")
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Flow observation migration
90
+
91
+ **Before**:
92
+ ```kotlin
93
+ @Composable
94
+ fun TodoScreen(vm: TodoViewModel = viewModel()) {
95
+ val todos by vm.todos.collectAsState()
96
+ Column { todos.forEach { Text(it.title) } }
97
+ }
98
+ ```
99
+
100
+ **After (new stub)**:
101
+ ```kotlin
102
+ @KetoyCapabilityStub(id = 0x4000, name = "OBSERVE_TODOS")
103
+ public fun observeTodos(): Flow<List<Any?>> = error(STUB_MSG)
104
+ ```
105
+
106
+ **After (KBC code)**:
107
+ ```kotlin
108
+ @KetoyEntryPoint @KetoyComposable @Composable
109
+ fun TodoScreen() {
110
+ val todos = observeTodos().collectAsState(initial = emptyList()).value
111
+ Column {
112
+ todos.forEach { todo ->
113
+ val title = (todo as Map<String, Any?>)["title"] as String
114
+ Text(title)
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ **Host (Hilt module)**:
121
+ ```kotlin
122
+ KBCRoomBridge(registry).observeList(0x4000) { _ ->
123
+ todoRepository.observeAll() // Flow<List<TodoEntity>>
124
+ .map { list -> list.map { it.toMap() } } // Flow<List<Map<String, Any?>>>
125
+ }
126
+ ```
127
+
128
+ ### Network call migration
129
+
130
+ **Before**:
131
+ ```kotlin
132
+ val response = httpClient.get("https://api.example.com/users/$id")
133
+ ```
134
+
135
+ **After**:
136
+ ```kotlin
137
+ // In Capabilities.kt:
138
+ @KetoyCapabilityStub(id = 0x4010, name = "FETCH_USER")
139
+ public suspend fun fetchUser(id: Long): Map<String, Any?> = error(STUB_MSG)
140
+
141
+ // In screen:
142
+ LaunchedEffect(userId) {
143
+ val user = fetchUser(userId)
144
+ vmSetState("user", user)
145
+ }
146
+ ```
147
+
148
+ **Host**:
149
+ ```kotlin
150
+ registry.registerSuspend(0x4010) { args ->
151
+ val id = args[0] as Long
152
+ httpClient.get("https://api.example.com/users/$id").body
153
+ }
154
+ ```
155
+
156
+ ### Navigation migration
157
+
158
+ **Before**:
159
+ ```kotlin
160
+ val navController = LocalNavController.current
161
+ Button(onClick = { navController.navigate("detail/$id") }) { ... }
162
+ ```
163
+
164
+ **After**:
165
+ ```kotlin
166
+ Button(onClick = { navPush("detail/$id") }) { ... }
167
+ ```
168
+
169
+ (Host registers `NAV_PUSH` via `registerNavigationCapabilities(...)` already.)
170
+
171
+ ### Lazy lists — fallback strategy
172
+
173
+ LazyColumn / LazyRow are not yet supported. Options:
174
+
175
+ - **Small lists**: Use `Column` with explicit `forEach`. Performance acceptable up to ~50 items.
176
+ - **Large lists**: Keep the screen native and have the KBC screen navigate to it via `navPush("nativeList/<args>")`. Native screens read the same Hilt-injected repository as the host-registered capabilities.
177
+ - **Pagination**: Implement server-side and pass current page items via state.
178
+
179
+ ## When NOT to migrate
180
+
181
+ Recommend keeping a screen native if it:
182
+
183
+ - Uses `LazyColumn` or `LazyVerticalGrid` for >50 items.
184
+ - Has gesture handling (`pointerInput`, swipe-to-dismiss, custom drag).
185
+ - Uses Canvas / Path drawing.
186
+ - Embeds AndroidViews via `AndroidView { ... }`.
187
+ - Implements custom `Layout` or `SubcomposeLayout`.
188
+ - Has performance-critical animations (`Animatable`, `Transition`).
189
+
190
+ Migration adds compile complexity and runtime indirection. If the screen isn't part of a server-driven update strategy (A/B tests, hot patches, feature rollouts), there's no benefit to shipping it as KBC.
@@ -0,0 +1,46 @@
1
+ # Publishing Bundles (DEFERRED)
2
+
3
+ > **Status**: The Ketoy publishing backend is not GA. Don't write publishing code yet. This document captures the intended flow so the agent can describe it on request without inventing details.
4
+
5
+ ## Why this is deferred
6
+
7
+ Server-side bundle delivery requires:
8
+ - A trusted CDN with per-app namespace isolation.
9
+ - Signed-bundle validation pipeline (verify on upload, reject mismatched keys).
10
+ - Version manifests so clients know "is there a newer bundle?".
11
+ - A/B test routing and rollout controls.
12
+ - Audit logs for which bundle each user received.
13
+
14
+ None of that is shipping yet. Until it does, the answer to "how do I publish?" is: **build, sign, upload to your own CDN, point `KetoyBundleSource.Remote(url)` at it.**
15
+
16
+ ## Future CLI shape (do NOT implement)
17
+
18
+ ```
19
+ ketoy publish --package com.example.app
20
+ # Prompts:
21
+ # "You are about to publish to 'com.example.app'. Confirm by typing the package name: "
22
+ # <user types it exactly>
23
+ # Then:
24
+ # - reads `app/src/main/assets/ketoy/main.ktx`
25
+ # - re-verifies signature against the registered public key
26
+ # - uploads to ketoy.dev/api/bundles/com.example.app/{version}
27
+ # - returns a deployment ID + signed URL
28
+ ```
29
+
30
+ The written-package-name confirmation is a safety gate — copy-pasting accidentally is the easiest way to push the wrong bundle to production.
31
+
32
+ ## Until then
33
+
34
+ If a user asks "how do I publish?":
35
+
36
+ 1. Confirm they want CDN delivery (vs. APK-bundled).
37
+ 2. Point them at `KetoyBundleSource.Remote(url, headers)` in `reference/`.
38
+ 3. Explain the ETag-based caching: their CDN's `ETag` response header drives `If-None-Match` revalidation on the host.
39
+ 4. Recommend a CI step:
40
+ ```bash
41
+ ./gradlew :app:ketoyBundle --rerun-tasks
42
+ aws s3 cp app/src/main/assets/ketoy/main.ktx s3://your-cdn/bundles/com.example.app/$(git rev-parse HEAD).ktx
43
+ ```
44
+ 5. Recommend signature verification ON in production (`enableSignatureVerification = true` + matching public key shipped in APK).
45
+
46
+ Do **not** offer to write the upload pipeline. That's the user's CI / CDN choice.
@@ -0,0 +1,141 @@
1
+ # Safe Edits Policy — Never Rewrite Existing Files
2
+
3
+ **Hard rule**: when modifying existing files (`build.gradle.kts`, `AndroidManifest.xml`, `MainActivity.kt`, `Application` class, `settings.gradle.kts`, etc.) the agent MUST make **surgical, additive edits** at specific locations. Never write/overwrite the whole file.
4
+
5
+ A full rewrite of a user's `app/build.gradle.kts` will destroy their existing dependencies, signing config, build types, flavors, ProGuard rules, Crashlytics integration, custom tasks, and anything else. Treat every existing file as sacred. Templates in `templates/` are reference scaffolds — they describe the final shape, NOT the bytes to write.
6
+
7
+ ## How to make a safe edit
8
+
9
+ For every file the user already has:
10
+
11
+ 1. **Read** the file in full.
12
+ 2. **Locate the insertion point** by pattern matching on stable anchors (e.g. `plugins {`, `dependencies {`, `android {`, `<application`, `class MainActivity`, `override fun onCreate`).
13
+ 3. **Check for prior Ketoy entries** — if `id("dev.ketoy.compiler")` already exists in `plugins { }`, don't add it again. Same for every dependency, every annotation, every import.
14
+ 4. **Make the minimal Edit** — single-line additions, or single-block appends. Never delete user content.
15
+ 5. **Show a diff** before confirming the change. Get explicit acknowledgment when:
16
+ - Modifying a file the user is likely to have hand-tuned (MainActivity, Application, build.gradle.kts).
17
+ - Touching anything that affects packaging (Manifest, ProGuard, signing).
18
+
19
+ ## File-by-file insertion strategy
20
+
21
+ ### `app/build.gradle.kts`
22
+
23
+ Three surgical edits:
24
+
25
+ **a. Add plugin** — find the `plugins { ... }` block, insert `id("dev.ketoy.compiler")` at the end of its body. Verify it's not already present.
26
+
27
+ ```kotlin
28
+ plugins {
29
+ alias(libs.plugins.android.application)
30
+ alias(libs.plugins.kotlin.android)
31
+ alias(libs.plugins.kotlin.compose)
32
+ // ... user's existing plugins ...
33
+ id("dev.ketoy.compiler") // ← single line added
34
+ }
35
+ ```
36
+
37
+ **b. Add dependencies** — find the `dependencies { ... }` block, append a `// Ketoy` section at the end of the body:
38
+
39
+ ```kotlin
40
+ dependencies {
41
+ // ... user's existing dependencies, untouched ...
42
+
43
+ // Ketoy
44
+ implementation(platform("dev.ketoy.vm:ketoy-bom:0.2.1-alpha"))
45
+ implementation("dev.ketoy.vm:ketoy-runtime")
46
+ implementation("dev.ketoy.vm:ketoy-capabilities-core")
47
+ implementation("dev.ketoy.vm:ketoy-capabilities-navigation")
48
+ implementation("dev.ketoy.vm:ketoy-adapters-material3")
49
+ implementation("dev.ketoy.vm:ketoy-annotations")
50
+ // implementation("dev.ketoy.vm:ketoy-hilt") // add only if user uses Hilt
51
+ }
52
+ ```
53
+
54
+ **c. Append the `ketoy { }` block** at file bottom (after the last `}`). Don't insert mid-file.
55
+
56
+ ```kotlin
57
+ // existing file ends here.
58
+ // Then append:
59
+ ketoy {
60
+ exportFromAppModule.set(true)
61
+ bundleId.set("main")
62
+ bundleVariant.set("release")
63
+ capabilityRegistryFile.set(file("ketoy-capabilities.json"))
64
+ debugMode.set(true)
65
+ val signingKey = file("keys/sample-private.key")
66
+ if (signingKey.exists()) {
67
+ signingKeyFile.set(signingKey)
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Never touch**: `android { }`, `defaultConfig { }`, `buildTypes { }`, `compileOptions { }`, `kotlinOptions { }`, `buildFeatures { }`, `packagingOptions`, signing configs, or any custom block the user wrote. If `minSdk < 26`, **stop and ask** — don't silently bump it.
73
+
74
+ ### `AndroidManifest.xml`
75
+
76
+ Single surgical edit: add `android:name` attribute to the existing `<application>` tag. Do NOT replace the whole `<application>` block — the user has `android:icon`, `android:theme`, `android:label`, `android:allowBackup`, possibly `tools:replace`, network security config, etc.
77
+
78
+ If `android:name` already exists pointing at a non-Ketoy Application class → tell the user and ask whether to make their existing Application class extend our setup, or whether to point at a new class.
79
+
80
+ ### `MainActivity.kt`
81
+
82
+ Three scenarios:
83
+
84
+ **a. File doesn't exist** — safe to write from `examples/hilt-config.kt` or `examples/no-hilt-config.kt`.
85
+
86
+ **b. File exists and is the boilerplate Android Studio template** (typically just sets up a `setContent { GreetingTheme { Greeting("Android") } }`) — show a diff, ask, then replace with the Ketoy-aware version.
87
+
88
+ **c. File exists and contains user code** — **do NOT replace**. Instead:
89
+ - Add the necessary imports (single Edit).
90
+ - Add the `@AndroidEntryPoint` annotation if Hilt and missing (single Edit).
91
+ - Add `@Inject lateinit var ...` field declarations (single Edit block inside the class).
92
+ - **Don't touch `onCreate`** — the user might be doing complex setup. Instead, point them at the example and say: "Here's how to integrate `KetoyScreen` inside your existing `setContent { }`. Make the edit yourself, or paste this block:" and show the snippet.
93
+
94
+ ### `Application` class
95
+
96
+ **a. File doesn't exist** — safe to create new (`App.kt` or `MyApplication.kt`).
97
+
98
+ **b. User already has an `Application` subclass** — **don't replace**. Edit it surgically:
99
+ - Add `@HiltAndroidApp` if Hilt and missing.
100
+ - For non-Hilt: add the `ketoyRuntime` / `ketoyBundleLoader` field declarations and initialization inside `onCreate()` — as an Edit that inserts the new lines AFTER `super.onCreate()` without disturbing the user's existing init code.
101
+
102
+ ### `settings.gradle.kts`
103
+
104
+ Only edit if `mavenCentral()` is missing from `pluginManagement.repositories` or `dependencyResolutionManagement.repositories`. Single-line Edit appending `mavenCentral()` to whichever block lacks it.
105
+
106
+ ### `.gitignore`
107
+
108
+ Single-line append: `**/keys/*-private.key`. Check it's not already there.
109
+
110
+ ## New files — always fair game
111
+
112
+ Files that the user does NOT have an existing version of can be created in full:
113
+
114
+ - `app/ketoy-capabilities.json` (new)
115
+ - `app/src/main/.../ketoyscreens/Capabilities.kt` (new file in new directory)
116
+ - `app/src/main/.../ketoyscreens/HelloKetoyScreen.kt` or migrated screens (new files)
117
+ - `app/src/main/.../di/AppCapabilityProvider.kt` (new, Hilt only)
118
+ - `app/src/main/.../di/AppHiltModule.kt` (new, Hilt only — UNLESS user already has an `AppHiltModule` / `AppModule`, in which case insert into theirs)
119
+
120
+ For Hilt module insertion when the user has their own: append `@Binds` for `KetoyCapabilityProvider` and `@Provides` for the three resolvers + the config customizer — as additive entries in the existing module. Don't introduce a parallel module unless the user's existing one is `internal` / not in scope.
121
+
122
+ ## The diff-and-confirm checkpoint
123
+
124
+ Before any edit that touches `build.gradle.kts`, `AndroidManifest.xml`, `MainActivity.kt`, or an existing `Application` class:
125
+
126
+ 1. Show the user the proposed diff (using `Edit`'s natural diff output).
127
+ 2. Wait for confirmation. Don't batch multiple file edits without checking in.
128
+ 3. If the user denies, ask what they'd prefer and adjust.
129
+
130
+ For the easy stuff (new files in `ketoyscreens/`, capability registry JSON, fresh Hilt provider) you can proceed without the diff-and-confirm dance — but always print what was created.
131
+
132
+ ## If the project is unusual
133
+
134
+ If grepping turns up things like:
135
+
136
+ - A custom `Application` that extends `MultiDexApplication` or a third-party class
137
+ - An existing `KetoyScreen` integration (someone already started)
138
+ - Build scripts in Groovy (`build.gradle`, not `.kts`)
139
+ - KMP project structure with `androidApp` instead of `app`
140
+
141
+ Stop. Describe what you found, and ask the user how they want to proceed. Don't guess.
@@ -0,0 +1,122 @@
1
+ # Architecture cheatsheet — for AI grounding
2
+
3
+ When reasoning about Ketoy code, keep this layered model in mind:
4
+
5
+ ```
6
+ ┌───────────────────────────────────────────────────────┐
7
+ │ 1. Kotlin source (.kt) │
8
+ │ @KetoyComposable + @KetoyEntryPoint + @KetoyViewModel
9
+ │ @KetoyCapabilityStub (compile-time only) │
10
+ ├───────────────────────────────────────────────────────┤
11
+ │ 2. Compiler plugin (K2 IR → KBC bytecode) │
12
+ │ KetoyIRGenerationExtension + KBCEmitter │
13
+ │ CapabilityValidator does transitive closure walk │
14
+ ├───────────────────────────────────────────────────────┤
15
+ │ 3. .ktx bundle │
16
+ │ Brotli-compressed, Ed25519-signed │
17
+ │ Sections: string pool, manifests, function table, │
18
+ │ code, modifier table, bundle metadata, entries │
19
+ ├───────────────────────────────────────────────────────┤
20
+ │ 4. VM interpreter (register-based) │
21
+ │ KBCInterpreter + KBCCoroutineEngine │
22
+ │ Tier-1 DexMaker JIT (API 26+) │
23
+ ├───────────────────────────────────────────────────────┤
24
+ │ 5. Compose renderer │
25
+ │ KBCComposeEngine + adapter / constructor registries│
26
+ │ Native Compose slot table via registered adapters │
27
+ └───────────────────────────────────────────────────────┘
28
+ ```
29
+
30
+ ## Module dependency order (small → large)
31
+
32
+ ```
33
+ ketoy-annotations (KMP — pure annotations, zero deps)
34
+
35
+ ketoy-bytecode (KMP — KBC opcodes + KBCValue + bundle data types)
36
+
37
+ ketoy-bundle-format (KMP — KtxReader/Writer, signing, compression)
38
+
39
+ ketoy-runtime (Android AAR — interpreter, ViewModels, Compose engine)
40
+
41
+ ketoy-capabilities-core (Android AAR — HTTP, KV, dispatchers, navigation registration)
42
+ ketoy-capabilities-navigation (Android AAR — navigator integration)
43
+ ketoy-adapters-material3 (Android AAR — Material3 adapters + constructors)
44
+ ketoy-hilt (Android AAR — Hilt modules + factory builder)
45
+ ketoy-test (Android AAR — fakes + KBCBuilder DSL)
46
+
47
+ ketoy-compiler-plugin (JVM jar — K2 compiler plugin)
48
+ ketoy-gradle-plugin (JVM jar — wires compiler plugin + ketoyBundle task)
49
+ ```
50
+
51
+ ## Key data types to recognize
52
+
53
+ | Type | What it is | Where it lives |
54
+ |---|---|---|
55
+ | `@KetoyComposable` | Marks a function for KBC compilation | `ketoy-annotations` |
56
+ | `@KetoyEntryPoint` | Marks a function as an entry-point (host can name-lookup) | `ketoy-annotations` |
57
+ | `@KetoyCapabilityStub` | Compile-time bridge to a host capability | `ketoy-annotations` |
58
+ | `KetoyConfig` | Runtime config (sig verification, JIT, resolvers) | `ketoy-runtime` |
59
+ | `KetoyRuntime` | Top-level runtime entry — `parseBundle` + `createVM` | `ketoy-runtime` |
60
+ | `KetoyBundleLoader` | Loads `.ktx` from Asset/Raw/Remote/Preloaded | `ketoy-runtime` |
61
+ | `KetoyBundleSource` | Sealed source variants (Asset, Remote, Raw, Preloaded) | `ketoy-runtime` |
62
+ | `KetoyScreen` | `@Composable` entry point for host code | `ketoy-runtime` |
63
+ | `KetoyVirtualViewModel` | Per-bundle ViewModel hosting the VM | `ketoy-runtime` |
64
+ | `CapabilityRegistry` | Host-side registry of capability IDs → callbacks | `ketoy-runtime` |
65
+ | `KetoyCapabilityProvider` | Hilt interface for building the registry | `ketoy-hilt` |
66
+ | `KetoyHiltProvider` | `@Composable` Hilt-context publisher | `ketoy-hilt` |
67
+ | `KetoyConfigCustomizer` | Hilt `fun interface` to customize default config | `ketoy-hilt` |
68
+ | `MaterialIconsResolver` | Host registry for `Icons.<Style>.<Name>` | `ketoy-adapters-material3` |
69
+ | `MaterialFontFamilyResolver` | Host registry for `R.font.X` | `ketoy-adapters-material3` |
70
+ | `MaterialDrawableResolver` | Host registry for `R.drawable.X` | `ketoy-adapters-material3` |
71
+ | `KBCRoomBridge` | Helper for Room DAO → capability registration | `ketoy-capabilities-core` |
72
+
73
+ ## Bundle lifecycle (load path)
74
+
75
+ ```
76
+ KetoyScreen
77
+ └─> KetoyBundleLoader.load(KetoyBundleSource.Asset("ketoy/main.ktx"))
78
+ ↓ reads bytes from APK assets
79
+ └─> KetoyRuntime.parseBundle(bytes)
80
+ ↓ optional: Ed25519Verifier.verify(bytes, publicKey)
81
+ ↓ KtxReader.read(bytes) → KetoyBundle
82
+ ↓ BundleValidator.validate(bundle, adapter/constructor/capability registries)
83
+ ↓ (STRICT — every manifest entry must be registered)
84
+ ↓ cache by SHA-256(bytes) for future loads
85
+ └─> KetoyVirtualViewModel.Factory(bundle, extras, registries, config)
86
+ ↓ creates per-bundle VM with viewModelScope
87
+ └─> KetoyVM.executeComposable(entryPointFnIdx)
88
+ ↓ interpreter walks bytecode, dispatches:
89
+ ↓ COMPOSABLE_CALL → adapterRegistry.invoke(id, params)
90
+ ↓ CONSTRUCT_JVM → constructorRegistry.construct(id, params)
91
+ ↓ INVOKE_CAPABILITY[_SUSPEND] → registry.invoke(id, args)
92
+ ```
93
+
94
+ ## Compile lifecycle
95
+
96
+ ```
97
+ .kt source with @KetoyComposable
98
+ └─> Kotlin K2 frontend (normal)
99
+ └─> KetoyIRGenerationExtension (compiler plugin)
100
+ ↓ KetoyDeclarationCollector — scans for @Ketoy* annotations
101
+ ↓ CapabilityValidator.validate(root, allTopLevelFns)
102
+ ↓ - transitive closure walk over @KetoyComposable roots
103
+ ↓ - native sibling top-level fns untouched (compile to DEX)
104
+ ↓ - errors carry `Reached via:` breadcrumbs
105
+ ↓ KBCEmitter.emit(root, captures)
106
+ ↓ - IR nodes → KBC instructions
107
+ ↓ - ClosureAnalyzer finds outer-scope captures → KBCValue.ClosureRef
108
+ ↓ - Compose token reads → ComposeTokenRegistry → KBCValue.*Id literals
109
+ ↓ - Modifier chains → KBCModifierIRWalker → modifier table
110
+ ↓ - Atomic-collapse encoder shortcuts (Color.X, R.font.X, etc.)
111
+ └─> KtxBundleBuilder accumulates functions + manifests
112
+ └─> KtxWriter.write(bundle, optional signer) → bytes
113
+ └─> KetoyBundleTask writes to app/src/main/assets/ketoy/main.ktx
114
+ ```
115
+
116
+ ## What's at each ID range
117
+
118
+ - `0x0000 – 0x0FFF`: Reserved / standard Compose adapters + constructors (Material3 + Foundation + Coil)
119
+ - `0x4000+`: App-specific (your custom composables, capabilities, constructors)
120
+ - `0xFFFF`: Sentinel / never used
121
+
122
+ Stable forever; never re-assign.
@@ -0,0 +1,122 @@
1
+ # Capabilities Catalog
2
+
3
+ Capabilities are how KBC source calls back into the Android host. Each capability has a stable `Short` ID and is registered into a `CapabilityRegistry` once per host (via Hilt module or `Application.onCreate`).
4
+
5
+ Inside KBC source, capabilities are referenced through `@KetoyCapabilityStub`-annotated functions whose bodies throw — the compiler plugin replaces each call with `INVOKE_CAPABILITY` / `INVOKE_CAPABILITY_SUSPEND` / `INVOKE_CAPABILITY_FLOW`.
6
+
7
+ ## Standard library capability IDs
8
+
9
+ Ranges:
10
+
11
+ | Range | Domain | Examples |
12
+ |---|---|---|
13
+ | `0x0001 – 0x08FF` | UI / modifier infrastructure (reserved, internal) | |
14
+ | `0x0500 – 0x05FF` | Network | `HTTP_GET`, `HTTP_POST`, `SSE_SUBSCRIBE` |
15
+ | `0x0600 – 0x06FF` | Storage | `KV_GET/SET/OBSERVE`, `DB_QUERY/OBSERVE` |
16
+ | `0x0700 – 0x07FF` | Navigation | `NAV_PUSH`, `NAV_POP`, `NAV_REPLACE`, `NAV_DEEP_LINK` |
17
+ | `0x0900 – 0x09FF` | Platform | `TOAST`, `VIBRATE`, `CLIPBOARD_SET/GET`, `OPEN_URL`, `LOG`, `ANALYTICS_TRACK` |
18
+ | `0x0A00 – 0x0AFF` | ViewModel state | `VM_GET_STATE`, `VM_SET_STATE`, `VM_OBSERVE_STATE`, `VM_DISPATCH` |
19
+ | `0x0B00 – 0x0BFF` | Dispatchers | `DISPATCHER_IO`, `DISPATCHER_DEFAULT`, `DISPATCHER_MAIN` |
20
+ | `0x0C00 – 0x0CFF` | Flow operators | `FLOW_MAP`, `FLOW_FILTER`, `FLOW_COMBINE`, `FLOW_DEBOUNCE`, `STATE_FLOW_CREATE` |
21
+ | `0x4000+` | App-specific | Anything your host registers |
22
+
23
+ Full list lives in `ketoy-capabilities-core/.../CapabilityIds.kt`. Read that file when in doubt.
24
+
25
+ ## Common stubs (paste into your KBC source)
26
+
27
+ The stubs below are conventional — the IDs and names match the standard library. Place in `app/src/main/.../ketoyscreens/Capabilities.kt`.
28
+
29
+ ```kotlin
30
+ @file:Suppress("UnusedParameter") // Stub bodies discard params; compiler reads the signature.
31
+ package com.example.app.ketoyscreens
32
+
33
+ import dev.ketoy.annotations.KetoyCapabilityStub
34
+ import kotlinx.coroutines.flow.Flow
35
+
36
+ private const val STUB_MSG = "Ketoy capability stub — replaced by INVOKE_CAPABILITY at compile time"
37
+
38
+ // Navigation
39
+ @KetoyCapabilityStub(id = 0x0700, name = "NAV_PUSH")
40
+ public fun navPush(route: String): Unit = error(STUB_MSG)
41
+
42
+ @KetoyCapabilityStub(id = 0x0701, name = "NAV_POP")
43
+ public fun navPop(): Unit = error(STUB_MSG)
44
+
45
+ @KetoyCapabilityStub(id = 0x0702, name = "NAV_REPLACE")
46
+ public fun navReplace(route: String): Unit = error(STUB_MSG)
47
+
48
+ // ViewModel state bridge
49
+ @KetoyCapabilityStub(id = 0x0A00, name = "VM_GET_STATE")
50
+ public fun vmGetState(key: String): Any? = error(STUB_MSG)
51
+
52
+ @KetoyCapabilityStub(id = 0x0A01, name = "VM_SET_STATE")
53
+ public fun vmSetState(key: String, value: Any?): Unit = error(STUB_MSG)
54
+
55
+ @KetoyCapabilityStub(id = 0x0A02, name = "VM_OBSERVE_STATE")
56
+ public fun vmObserveState(key: String): Flow<Any?> = error(STUB_MSG)
57
+
58
+ // Platform
59
+ @KetoyCapabilityStub(id = 0x0900, name = "ANALYTICS_TRACK")
60
+ public fun analyticsTrack(event: String, props: Map<String, Any?>): Unit = error(STUB_MSG)
61
+
62
+ @KetoyCapabilityStub(id = 0x0901, name = "TOAST")
63
+ public fun toast(message: String, duration: Int = 0): Unit = error(STUB_MSG)
64
+ ```
65
+
66
+ ## Defining an app-specific capability
67
+
68
+ Step 1 — declare the stub:
69
+
70
+ ```kotlin
71
+ @KetoyCapabilityStub(id = 0x4001, name = "FETCH_USER_PROFILE")
72
+ public suspend fun fetchUserProfile(userId: Long): Map<String, Any?> = error(STUB_MSG)
73
+ ```
74
+
75
+ Step 2 — register host-side (inside your `KetoyCapabilityProvider.buildRegistry()`):
76
+
77
+ ```kotlin
78
+ override fun buildRegistry(): CapabilityRegistry {
79
+ val registry = CapabilityRegistry()
80
+ registry.registerCoreCapabilities(context, httpClient = httpClient)
81
+ registry.registerSuspend(0x4001) { args ->
82
+ val userId = args[0] as Long
83
+ userProfileRepository.fetch(userId) // returns Map<String, Any?>
84
+ }
85
+ return registry
86
+ }
87
+ ```
88
+
89
+ Step 3 — declare it in the capability registry JSON for compile-time validation (`app/ketoy-capabilities.json`):
90
+
91
+ ```json
92
+ {
93
+ "version": 1,
94
+ "allowedStdlibFqNames": [],
95
+ "capabilities": [
96
+ { "id": 16385, "name": "FETCH_USER_PROFILE",
97
+ "fqName": "com.example.app.ketoyscreens.fetchUserProfile",
98
+ "kind": "SUSPEND", "required": true }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ (`0x4001` = `16385` decimal.)
104
+
105
+ ## Registration flavors
106
+
107
+ | Flavor | When to use | Wire op |
108
+ |---|---|---|
109
+ | `register(id) { args -> Any? }` | Pure sync, fire-and-forget or returns immediately | `INVOKE_CAPABILITY` |
110
+ | `registerSuspend(id) { args -> Any? }` | Network calls, DB writes, anything that suspends | `INVOKE_CAPABILITY_SUSPEND` |
111
+ | `registerFlow(id) { args -> Flow<Any?> }` | DB observes, SSE streams, state subscriptions | `INVOKE_CAPABILITY_FLOW` |
112
+ | `KBCRoomBridge.observeList(id) { args -> Flow<List<...>> }` | Room DAO `Flow<List<X>>` queries | flow wrapper |
113
+
114
+ ## Validation timing
115
+
116
+ | When | What's checked |
117
+ |---|---|
118
+ | Compile (compiler plugin) | Stub FQ name → capability ID lookup, signature shape |
119
+ | Bundle load (`BundleValidator`) | Every manifest entry in the `.ktx` exists in the host's `CapabilityRegistry` (STRICT — even `required = false` entries are checked) |
120
+ | Runtime (`INVOKE_CAPABILITY`) | Args match the registered callback's expectations (you write the cast) |
121
+
122
+ So a missing host registration explodes at bundle load, not at the call site — diagnostics point at the bundle, not the screen.