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.
- package/LICENSE +17 -0
- package/README.md +101 -0
- package/SECURITY.md +34 -0
- package/dist/ketoy.js +3165 -0
- package/dist/ketoy.js.map +1 -0
- package/package.json +78 -0
- package/skills/ketoy/README.md +50 -0
- package/skills/ketoy/SKILL.md +148 -0
- package/skills/ketoy/examples/capabilities-stubs.kt +60 -0
- package/skills/ketoy/examples/hilt-config.kt +192 -0
- package/skills/ketoy/examples/no-hilt-config.kt +101 -0
- package/skills/ketoy/examples/todo-screen.kt +156 -0
- package/skills/ketoy/guides/build-and-analyze.md +87 -0
- package/skills/ketoy/guides/diagnose-errors.md +129 -0
- package/skills/ketoy/guides/init-project.md +127 -0
- package/skills/ketoy/guides/migrate.md +190 -0
- package/skills/ketoy/guides/publish-deferred.md +46 -0
- package/skills/ketoy/guides/safe-edits.md +141 -0
- package/skills/ketoy/reference/architecture-cheatsheet.md +122 -0
- package/skills/ketoy/reference/capabilities.md +122 -0
- package/skills/ketoy/reference/forbidden-apis.md +149 -0
- package/skills/ketoy/reference/supported-composables.md +80 -0
- package/skills/ketoy/reference/supported-constructors.md +57 -0
- package/skills/ketoy/reference/supported-modifiers.md +76 -0
- package/skills/ketoy/templates/app-build.gradle.kts.tmpl +109 -0
- package/skills/ketoy/templates/ketoy-capabilities.json.tmpl +21 -0
- package/skills/ketoy/templates/manifest-snippet.xml.tmpl +33 -0
- package/templates/HelloKetoyScreen.kt.tmpl +51 -0
- package/templates/MainActivity.kt.tmpl +53 -0
- package/templates/MyApplication.kt.tmpl +88 -0
- 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.
|