Skip to content

Commit 24e8db5

Browse files
RFC: Android Kotlin → Rust/Gossamer migration (#97)
## Status **DRAFT — RFC only. Awaiting owner review BEFORE any bulk conversion.** No Kotlin deleted, no Gossamer scaffolding generated, no behaviour change. Supersedes #96 (was titled "Tauri 2" — owner correction: Gossamer is the estate target). ## Why `android/` ships 7 Kotlin files + 3 `*.gradle.kts` files, all banned by the Hyperpolymath language policy. Two CI gates permanently red: - `Check for Banned Languages` (`.github/workflows/language-policy.yml:87-94`) - `governance / Language / package anti-pattern policy` (estate-wide) ## What this PR contains A single document — `docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc` — mapping every Kotlin component to its Gossamer replacement and proposing a 9-PR landing sequence. ## Component mapping (one-line summary) | Kotlin file | Replacement | |---|---| | `MainActivity.kt` | `NeurophoneMainActivity extends GossamerActivity` | | `NeurophoneService.kt` | Hand-written Java `Service` shim → JNI into Rust | | `BootReceiver.kt` | Hand-written Java `BroadcastReceiver` shim → JNI | | `NativeLib.kt` | Delete; lives in `crates/neurophone-android/src/lib.rs` | | `NeurophoneAppWidget.kt` | Hand-written Java `AppWidgetProvider` shim | | `NeurophoneWidgetActions.kt` | Hand-written Java `BroadcastReceiver` shim | | `NeurophoneWidgetConfigureActivity.kt` | Java `Activity` shim (or drop) | | `*.gradle.kts` | Groovy `*.gradle` (matches Gossamer upstream) | ## Key structural finding **Gossamer Android is webview-only today.** ROADMAP Phase 3 (Mobile) lists Android as *Partially Implemented* — only `GossamerActivity` + `GossamerBridge` exist upstream. The 7 platform-integration surfaces neurophone needs (service, widget, receivers, sensors, intent dispatch, prefs, deep-links) have **no upstream Gossamer support and no open Gossamer issues**. **Recommendation:** build all 7 downstream in neurophone's own `android/` tree as small Java shims that JNI immediately into Rust. Don't block neurophone on Gossamer upstream PRs. Estimated total shim surface: < 250 LoC of Java vs. current 926 LoC of Kotlin. ## Estate precedent - `panll` migrated `src-tauri/` → `src-gossamer/` (commit `598d537`, ADR-0001 in `b935727`). Webview UI unchanged; swapped `tauri` crate for `gossamer-rs`, `tauri.conf.json` for `gossamer.conf.json`, `cargo tauri build` for `deno task build` invoking the `gossamer` CLI. - panll is **desktop only** — its migration did not exercise foreground service or home-screen widget surfaces; neurophone will be the first. ## Sub-PR sequence (proposed) 1. **This PR** — RFC only. 2. CI exemption for `android/**/*.java` (coordinated with `hyperpolymath/standards`). 3. Gossamer scaffolding: `gossamer-rs` workspace dep, `gossamer.conf.json`, `NeurophoneMainActivity` Java shim loading placeholder HTML. 4. Port `NativeLib` Kotlin → `crates/neurophone-android/src/lib.rs`. 5. Port `NeurophoneService` → Java shim + Rust impl. 6. Port `BootReceiver`. 7. Port widget triple (provider / actions / configure). 8. AffineScript UI compiled to JS, loaded by Gossamer webview. 9. Delete legacy Kotlin `android/` tree — both CI gates green. PRs 2 & 3 sequenced; 4–7 parallelizable after 3; 8 needs 4; 9 gated on all. ## Open questions for owner 1. **CI exemption scope** — estate-wide via `hyperpolymath/standards` or neurophone-local? Linchpin question. 2. **Estate governance** — who owns the cross-repo anti-pattern policy callable in standards? 3. **Widget config sacrifice** — acceptable to drop the configure activity? 4. **Gradle DSL** — Groovy `*.gradle` (matches Gossamer upstream) or keep Kotlin `*.gradle.kts` under a separate exemption? 5. **Hardware test plan** — Oppo Reno 13 vs emulator + Termux? 6. **Upstream contribution** — build shims neurophone-local, or factor a `gossamer-android-services` companion as we go? ## Constraints honoured - All new files MPL-2.0 SPDX. - No new GitHub Actions added. - No Deno/npm/TypeScript changes. ## Test plan - [x] RFC document renders as AsciiDoc. - [ ] Owner reviews component mapping for completeness. - [ ] Owner picks CI-exemption scope (open Q1). - [ ] Owner resolves Gradle DSL choice (open Q4). - [ ] Sub-PR #2 (CI exemption) opens once approved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c660445 commit 24e8db5

2 files changed

Lines changed: 559 additions & 0 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= JNI Surface Audit: `NativeLib.kt``crates/neurophone-android`
4+
:toc:
5+
:toclevels: 2
6+
:revdate: 2026-06-02
7+
:status: Head-start audit for sub-PR #4 of [[RFC-ANDROID-KOTLIN-TO-RUST]]
8+
9+
Companion to `RFC-ANDROID-KOTLIN-TO-RUST.adoc`. Maps every `external fun`
10+
in `android/app/src/main/java/ai/neurophone/NativeLib.kt` to (a) the
11+
Rust function we'll export from `crates/neurophone-android/src/lib.rs`
12+
and (b) the underlying `neurophone-core` API it composes. Surfaces
13+
gaps that need new methods on `NeuroSymbolicSystem` before sub-PR #4
14+
can finish.
15+
16+
== Current state
17+
18+
* `crates/neurophone-android/src/lib.rs` = 6-line stub (`pub fn hello() -> &'static str`). No JNI exports yet.
19+
* `crates/neurophone-android/Cargo.toml` already declares `crate-type =
20+
["cdylib"]`, depends on `jni`, `neurophone-core`, `sensors`,
21+
`serde_json`, `tokio`, `chrono`. Foundation is correct.
22+
* `neurophone-core::NeuroSymbolicSystem` has 9 public methods. Coverage
23+
vs the Kotlin JNI surface is partial (table below).
24+
25+
== The mapping table
26+
27+
[cols="2,3,3,2", options="header"]
28+
|===
29+
| Kotlin `external fun` | JNI export (Rust) | Underlying `neurophone-core` | Gap
30+
31+
| `init(configJson: String?): Boolean`
32+
| `Java_ai_neurophone_NativeLib_init`
33+
| `serde_json::from_str::<SystemConfig>(json)` →
34+
`NeuroSymbolicSystem::new(cfg)?.initialize()?`
35+
| **None** — core supports this directly. JNI layer holds the system in
36+
a `OnceLock<Mutex<Option<NeuroSymbolicSystem>>>`.
37+
38+
| `start(): Boolean`
39+
| `Java_ai_neurophone_NativeLib_start`
40+
| **MISSING on core.** Need `NeuroSymbolicSystem::start(&mut self)`.
41+
| **Gap 1**: core has no `start`/`stop`/lifecycle methods. Either add
42+
them to `neurophone-core` or let "running" be a JNI-layer concept
43+
(a `bool` flag in the static state holder). RFC recommends the
44+
latter to keep `neurophone-core` ABI lean.
45+
46+
| `stop()`
47+
| `Java_ai_neurophone_NativeLib_stop`
48+
| Companion to `start` — same gap.
49+
| **Gap 1** (same).
50+
51+
| `processSensor(sensorType: Int, values: FloatArray, timestamp: Long, accuracy: Int): Boolean`
52+
| `Java_ai_neurophone_NativeLib_processSensor`
53+
| `NeuroSymbolicSystem::process_sensor_event(SensorEvent { sensor_type, timestamp_ms, values })`
54+
| **Gap 2**: sensor-type int→string mapping. Kotlin's `NativeLib.pushSensorEvent`
55+
wrapper maps `1 → "accelerometer"`, `4 → "gyroscope"`, `2 → "magnetometer"`,
56+
`5 → "light"`, `8 → "proximity"`. This mapping moves into the Rust JNI
57+
layer as a `const ID_TO_NAME: &[(jint, &str)]` lookup.
58+
`accuracy` argument in the Kotlin signature is currently unused by
59+
`process_sensor_event` — either drop it from the JNI signature (breaks
60+
Kotlin ABI) or accept and ignore (preserves ABI; recommended).
61+
62+
| `queryLocal(message: String): String`
63+
| `Java_ai_neurophone_NativeLib_queryLocal`
64+
| `NeuroSymbolicSystem::query(message, prefer_local=true, force_local=true)`
65+
| **Gap 3**: `query` signature. Current core
66+
`query()` exists at line 206 of `neurophone-core/src/lib.rs` but its
67+
exact signature isn't visible from the public-surface grep — needs
68+
verification. Likely needs a `force_local` flag added.
69+
70+
| `queryClaude(message: String): String`
71+
| `Java_ai_neurophone_NativeLib_queryClaude`
72+
| `NeuroSymbolicSystem::query(message, prefer_local=false, force_cloud=true)`
73+
| **Gap 3** (same — force_cloud flag).
74+
75+
| `query(message: String, preferLocal: Boolean): String`
76+
| `Java_ai_neurophone_NativeLib_query`
77+
| `NeuroSymbolicSystem::query(message, prefer_local)`
78+
| Should match core directly if the existing signature is
79+
`query(&mut self, message: &str, prefer_local: bool)`. Verify.
80+
81+
| `getNeuralContext(): String`
82+
| `Java_ai_neurophone_NativeLib_getNeuralContext`
83+
| **MISSING on core.** No `get_neural_context()` method.
84+
| **Gap 4**: `neurophone-core` exposes `get_state()` (returns
85+
`SystemState` struct) but not a stringified neural-context summary.
86+
The Kotlin service expects the format
87+
`"[NEURAL_STATE] salience=N.NN (synthetic) [/NEURAL_STATE]"`
88+
(per `NeurophoneService.kt:113`). Need a new method on
89+
`NeuroSymbolicSystem` or compose in the JNI layer from `get_state()`.
90+
91+
| `getState(): String`
92+
| `Java_ai_neurophone_NativeLib_getState`
93+
| `serde_json::to_string(&system.get_state())`
94+
| **None** — `SystemState` already `Serialize` (line 78 of
95+
`neurophone-core/src/lib.rs`). JNI layer just calls
96+
`serde_json::to_string`.
97+
98+
| `reset()`
99+
| `Java_ai_neurophone_NativeLib_reset`
100+
| **MISSING on core.** No `reset()` method.
101+
| **Gap 5**: requires either (a) replace the held instance with a
102+
fresh `NeuroSymbolicSystem::new(config)` or (b) add a
103+
`reset(&mut self)` method to core that re-initialises LSM/ESN/Bridge
104+
state in-place without losing the config. (a) is simpler.
105+
106+
| `isRunning(): Boolean`
107+
| `Java_ai_neurophone_NativeLib_isRunning`
108+
| Reads the JNI-layer "running" flag from **Gap 1**.
109+
| Resolves via Gap 1.
110+
|===
111+
112+
== Summary of gaps
113+
114+
[cols="1,3,3", options="header"]
115+
|===
116+
| # | Gap | Resolution
117+
118+
| 1 | No `start`/`stop`/`is_running` lifecycle on `NeuroSymbolicSystem`.
119+
| Keep as JNI-layer concept: a `bool` in the static state holder.
120+
Do NOT add lifecycle methods to `neurophone-core` (keeps the crate
121+
framework-agnostic). Sub-PR #4 includes this in
122+
`crates/neurophone-android/src/state.rs`.
123+
124+
| 2 | Sensor-type int→string mapping currently in Kotlin
125+
(`NativeLib.kt:83-90`).
126+
| Move into Rust JNI layer as a `const SENSOR_TYPE_NAMES: &[(i32, &str)]`
127+
lookup. Same 5 mappings (`1→accelerometer`, `4→gyroscope`,
128+
`2→magnetometer`, `5→light`, `8→proximity`).
129+
130+
| 3 | `query` signature **VERIFIED** as
131+
`query(&mut self, message: &str, prefer_local: bool) -> Result<InferenceResult, NeurophoneError>`
132+
(`neurophone-core/src/lib.rs:206`). The Kotlin `queryLocal` / `queryClaude` callers
133+
expect HARD routing; current `prefer_local` is a heuristic flag combined with a
134+
word-count complexity threshold (`should_use_local = prefer_local && complexity < local_threshold`).
135+
| Recommend: introduce
136+
`enum QueryRoute { Auto, ForceLocal, ForceCloud }` and refactor `query`
137+
to take it. `queryLocal` → `QueryRoute::ForceLocal`, `queryClaude` →
138+
`QueryRoute::ForceCloud`, `query(..., prefer_local)` →
139+
`if prefer_local { QueryRoute::Auto } else { QueryRoute::Auto }` with
140+
the prefer flag passed through. Single small refactor to `neurophone-core`.
141+
Alternative if owner wants zero core change: implement `queryLocal` /
142+
`queryClaude` purely in the JNI layer by toggling
143+
`system.config.local_threshold` before each call (1.0 = always local,
144+
0.0 = always cloud) then restoring — hacky but ABI-stable.
145+
146+
| 4 | No `get_neural_context()` on `NeuroSymbolicSystem`.
147+
| Add method that returns
148+
`format!("[NEURAL_STATE] salience={:.2} (synthetic) [/NEURAL_STATE]", state.salience)`
149+
— i.e. compose from `get_state()` in core. Keeps the format
150+
contract owned by Rust, not the Kotlin shim.
151+
152+
| 5 | No `reset()` on `NeuroSymbolicSystem`.
153+
| JNI-layer approach: replace the held instance via
154+
`*system_guard = Some(NeuroSymbolicSystem::new(saved_config)?)`. No
155+
core change required if we also stash the original `SystemConfig`
156+
in the static state holder.
157+
|===
158+
159+
== Suggested file layout for sub-PR #4
160+
161+
----
162+
crates/neurophone-android/src/
163+
├── lib.rs -- #[no_mangle] JNI exports only, one per Java method
164+
├── state.rs -- OnceLock<Mutex<Option<RuntimeState>>> + RuntimeState
165+
├── sensor_map.rs -- ID_TO_NAME table + name_from_id() helper
166+
└── error.rs -- JniError → jboolean / JString conversions
167+
----
168+
169+
`state.rs::RuntimeState`:
170+
171+
[source,rust]
172+
----
173+
pub struct RuntimeState {
174+
pub system: NeuroSymbolicSystem,
175+
pub config: SystemConfig, // for reset()
176+
pub running: bool, // for start()/stop()/isRunning()
177+
}
178+
----
179+
180+
== JNI signature mapping cheat-sheet
181+
182+
[cols="2,3", options="header"]
183+
|===
184+
| Kotlin `external fun` | Rust `#[no_mangle] pub extern "system"`
185+
186+
| `init(configJson: String?): Boolean`
187+
| `Java_ai_neurophone_NativeLib_init(env: JNIEnv, _cls: JClass, json: JString) -> jboolean`
188+
189+
| `start(): Boolean`
190+
| `Java_ai_neurophone_NativeLib_start(env: JNIEnv, _cls: JClass) -> jboolean`
191+
192+
| `stop()`
193+
| `Java_ai_neurophone_NativeLib_stop(env: JNIEnv, _cls: JClass)`
194+
195+
| `processSensor(Int, FloatArray, Long, Int): Boolean`
196+
| `Java_ai_neurophone_NativeLib_processSensor(env: JNIEnv, _cls: JClass, sensor_type: jint, values: jfloatArray, timestamp: jlong, accuracy: jint) -> jboolean`
197+
198+
| `query(String, Boolean): String`
199+
| `Java_ai_neurophone_NativeLib_query(env: JNIEnv, _cls: JClass, message: JString, prefer_local: jboolean) -> jstring`
200+
201+
| `getNeuralContext(): String` / `getState(): String`
202+
| `Java_ai_neurophone_NativeLib_getNeuralContext(env: JNIEnv, _cls: JClass) -> jstring`
203+
|===
204+
205+
== Estimated size of sub-PR #4
206+
207+
* `crates/neurophone-android/src/lib.rs` — ~150 LoC (11 JNI exports × ~10-15 LoC each, mostly env↔Rust string conversion + lock acquisition + error mapping).
208+
* `crates/neurophone-android/src/state.rs` — ~40 LoC.
209+
* `crates/neurophone-android/src/sensor_map.rs` — ~25 LoC.
210+
* `crates/neurophone-android/src/error.rs` — ~30 LoC.
211+
* New methods on `neurophone-core`: `get_neural_context()` (~10 LoC).
212+
* Tests: at least one JNI roundtrip test simulating `JNIEnv` for each
213+
string-returning fn.
214+
215+
**Total estimate**: ~250 LoC new Rust + ~10 LoC change to
216+
`neurophone-core` + tests. Manageable single PR.
217+
218+
== What this audit does NOT block
219+
220+
* Sub-PR #4 cannot start until sub-PRs #2 (CI exemption) and #3
221+
(Gossamer scaffolding) land, per the RFC sequence. This audit is a
222+
head-start so #4 is shovel-ready when its turn comes.
223+
* `NeuroSymbolicSystem::query` signature verified — see Gap 3 above.
224+
* No outstanding research items before sub-PR #4 can begin (apart from
225+
it being gated on #2 + #3 per the RFC).

0 commit comments

Comments
 (0)