From a641aa4fa66fc3f17ac1764334abd51c56e10c92 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:24:03 +0100 Subject: [PATCH 1/4] =?UTF-8?q?docs(migration):=20RFC=20=E2=80=94=20Androi?= =?UTF-8?q?d=20Kotlin=20=E2=86=92=20Rust/Tauri=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft RFC for owner review before any bulk conversion. Maps each of the 7 Kotlin files + 3 *.gradle.kts to a Rust/Tauri replacement, calls out the unavoidable JVM-bytecode surface (Service / BroadcastReceiver / AppWidgetProvider), and proposes a 9-PR landing sequence gated on CI-exemption coordination for Tauri-generated Java shims under src-tauri/gen/android/. No Kotlin deleted, no Tauri scaffolding generated — pure planning artefact. Both banned-language CI gates remain red until the sequenced sub-PRs land in order. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../RFC-ANDROID-KOTLIN-TO-RUST.adoc | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc diff --git a/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc new file mode 100644 index 0000000..619a618 --- /dev/null +++ b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// NOTE: Task instruction asked for PMPL-1.0-or-later, but the active +// local pre-commit hook (.git/hooks/pre-commit) requires MPL-2.0 for +// neurophone. The repo's existing Kotlin/manifest files use PMPL, so +// the policies disagree. MPL chosen here to land the RFC; see +// "Open questions for owner" for resolution. += RFC: Android Kotlin → Rust/Tauri Migration +:toc: +:toclevels: 3 +:revdate: 2026-06-02 +:status: DRAFT — awaiting owner review BEFORE any bulk conversion + +== Problem + +The `android/` directory contains 7 Kotlin files + 3 `*.gradle.kts` files, +all banned by the Hyperpolymath language policy: + +---- +android/settings.gradle.kts +android/build.gradle.kts +android/app/build.gradle.kts +android/app/src/main/java/ai/neurophone/MainActivity.kt +android/app/src/main/java/ai/neurophone/NeurophoneService.kt +android/app/src/main/java/ai/neurophone/BootReceiver.kt +android/app/src/main/java/ai/neurophone/NativeLib.kt +android/app/src/main/java/ai/neurophone/widget/NeurophoneAppWidget.kt +android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetActions.kt +android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetConfigureActivity.kt +---- + +This keeps two estate CI gates permanently red: + +* `Check for Banned Languages` (workflow `language-policy.yml`, step + `Check for Java/Kotlin files`) +* `governance / Language / package anti-pattern policy` + +Both are sourced from `.github/workflows/language-policy.yml:87-94` which +hard-fails on any `*.kt` / `*.kts` / `*.java` match. + +== Goal + +* Migrate the Android UI + service to Tauri 2.0 (Rust backend + AffineScript/web UI). +* Both CI checks turn green. +* Behaviour preserved: sensor capture, foreground presence, widgets, boot-start. +* Sub-PRs are small and reviewable; nothing bulk-deleted until owner approves + this RFC. + +== Component inventory + replacement mapping + +[cols="2,1,3,3", options="header"] +|=== +| Kotlin component | LoC | Current role | Replacement + +| `MainActivity.kt` +| 377 +| UI shell (status / input / response), sensor capture in-activity, permissions, + intent handling (MAIN, SEND, VIEW `neurophone://`, ASSIST, widget query). +| **Tauri 2** activity (`gen/android/`-scaffolded `MainActivity` extends + `TauriActivity`). UI markup authored in **AffineScript** compiling to + Deno-ESM, loaded by Tauri's webview. Rust commands expose + `init/start/stop/query/get_neural_context` via `#[tauri::command]`. + Sensor capture moves out of activity → service. + +| `NeurophoneService.kt` +| 162 +| `Service` subclass: wake-lock, accelerometer @ 50 Hz, salience window, + `Notification.Builder` foreground channel, 1 Hz widget tick, calls + `NativeLib.init/start/stop/pushSensorEvent/getNeuralContext`. +| **Rust foreground service via thin Java entry-point**. Service class + `NeurophoneRuntimeService extends Service` is auto-generated at build time + under `gen/android/app/src/main/java/` (Tauri convention — generated + Java, not hand-written Kotlin). Lifecycle hooks (`onCreate / onStartCommand + / onDestroy / onSensorChanged`) immediately `JNI_OnLoad` into Rust + fns in `crates/neurophone-android`. Salience window, sensor processing, + wake-lock release, and widget publishing all in Rust. + +| `BootReceiver.kt` +| 25 +| `BroadcastReceiver` for `BOOT_COMPLETED` + `LOCKED_BOOT_COMPLETED`; starts + service if widget was running pre-reboot. +| **Generated Java `BootReceiver extends BroadcastReceiver`** under + `gen/android/`. Single `onReceive` body: JNI into Rust + `crates/neurophone-android::boot_receive`. + +| `NativeLib.kt` +| 93 +| JNI surface declarations + sensor-type-string→int mapping. +| **DELETE entirely.** All JNI exports already live (as stubs) in + `crates/neurophone-android/src/lib.rs`. Sensor mapping moves to + Rust. + +| `NeurophoneAppWidget.kt` +| 152 +| `AppWidgetProvider` (home-screen widget); renders title/state/salience + progress bar/Ask button via `RemoteViews`; persists widget state to + `SharedPreferences`. +| **Generated Java `NeurophoneAppWidget extends AppWidgetProvider`** under + `gen/android/`. `onUpdate / onReceive` each contain ONE JNI call into + Rust. `RemoteViews` construction stays in Java (it has to — `RemoteViews` + cannot be assembled from JNI without round-tripping every setter), but + the *decision* of what to render (running flag, salience value, + description string) comes from Rust. Prefs storage moves to Rust via + `ndk-context`-mediated JNI calls or a Rust-side SQLite cache. + +| `NeurophoneWidgetActions.kt` +| 47 +| Lightweight `BroadcastReceiver` dispatching `PUBLISH_STATE` / + `FORCE_REFRESH` from non-widget callers. +| **Generated Java `NeurophoneWidgetActions extends BroadcastReceiver`**; + body JNIs into Rust dispatch fn. + +| `NeurophoneWidgetConfigureActivity.kt` +| 70 +| Widget config UI (one checkbox: local-only mode), saved to prefs. +| **Generated Java `NeurophoneWidgetConfigureActivity extends Activity`** + with programmatic layout. Saving local-only-mode goes through JNI to + Rust config storage. Acceptable alternative: drop the widget config + entirely — a single setting like "local-only" can move to the main + Tauri UI and a sensible default applied at widget install. + +| `*.gradle.kts` (×3) +| ~100 +| Project + app module build config. +| **Replaced by Tauri-generated `gen/android/**/*.gradle.kts`** at + `cargo tauri android init` time. These are platform-prescribed by + Android Gradle Plugin: there is no Rust replacement and Tauri vends + them. The CI exemption section below covers this. +|=== + +== The unavoidable JVM-bytecode surface + +Android instantiates the following by class name at platform boundaries. +They **cannot be implemented in pure Rust** because Android's class loader +demands a class implementing the listed Android framework superclass: + +* `Activity` (main + widget-configure) +* `Service` +* `BroadcastReceiver` (boot + widget actions + widget provider) + +Three resolution paths exist; this RFC proposes **(A)** and explicitly +*rejects* (B) and (C). Owner picks before sub-PR work begins. + +=== Path (A) PROPOSED — Tauri-generated Java shims under `gen/android/` + +* `cargo tauri android init` generates per-platform Java glue inside + `src-tauri/gen/android/` (analogous to how `node_modules/` is generated + but not authored). +* These files are **not Kotlin**, they are minimal Java each delegating + immediately to a single JNI function in Rust. +* Add `gen/android/` to the `Check for Java/Kotlin files` step's exemption + list — same exemption pattern as `node_modules/` for the + `Check for ReScript files` step. +* The `governance / Language / package anti-pattern policy` check is + estate-wide (`hyperpolymath/standards`); adding the exemption needs a + one-line change in the reusable workflow caller. Pre-coordination + required. +* **Outcome:** zero hand-written Kotlin/Java in the repo's source tree; + CI passes; behaviour preserved. + +=== Path (B) REJECTED — drop the widget surface + +* Replace home-screen widget with a quick-settings tile + persistent + notification. Both can be declared in `AndroidManifest.xml` and driven + from a service whose only Java shim is auto-generated. +* Pro: smaller JVM surface. +* Con: regresses a shipped, user-visible feature; the OS integration + doc (`docs/OS_INTEGRATION.adoc`) lists the home-screen widget as the + primary integration point. + +=== Path (C) REJECTED — JVM bytecode generation from Rust + +* Use `j4rs` / `dx-java` / manual `.class` emission to ship JVM classes + built from Rust at compile time. +* Pro: no Java source at all. +* Con: experimental, fragile, no maintained tooling for `AppWidgetProvider` + superclass binding; not worth the risk. + +== Sub-PR sequence + +Each is a small, reviewable PR. Numbers are PR ordering, not commit count. + +. **#1 (THIS PR — DRAFT, no code changes)** — RFC for owner review. No + Kotlin deleted, no Tauri scaffolding generated. +. **#2 — CI exemption for generated mobile shims.** Update + `.github/workflows/language-policy.yml` step `Check for Java/Kotlin + files` to exempt `src-tauri/gen/android/`. Coordinated with + `hyperpolymath/standards` for the reusable governance check. **Lands + before any Tauri scaffolding so CI doesn't go redder en route.** +. **#3 — Tauri scaffolding (no behaviour).** Run `cargo tauri android + init`; commit `src-tauri/` + generated `gen/android/`. No deletion of + the existing `android/` Gradle project; both exist side by side so we + can A/B sanity-check. +. **#4 — Port `NativeLib` to Rust.** Fill out + `crates/neurophone-android/src/lib.rs` with the 10 JNI exports the + current Kotlin `NativeLib` declares. Wire to existing crates + (`neurophone-core`, `sensors`, `lsm`, `esn`, `bridge`, `llm`, + `claude-client`). Tests: JNI roundtrip via a Rust test that simulates + `JNIEnv`. +. **#5 — Port `NeurophoneService` to Rust.** Java shim under `gen/android/` + delegates `onCreate / onStartCommand / onDestroy / onSensorChanged` to + Rust. Salience window, wake-lock plumbing, widget tick — all Rust. +. **#6 — Port `BootReceiver` to Rust.** Same shim pattern, single fn. +. **#7 — Port widget triple (provider / actions / configure) to Rust.** + Java shims under `gen/android/` are minimal; widget render logic + (selecting strings, salience-pct, what `RemoteViews` setters to call) + is decided in Rust and pushed back to the shim via a single + data-bearing JNI fn. +. **#8 — AffineScript UI** (replaces `activity_main.xml` view binding). + Tauri loads the AffineScript-compiled UI in webview; `#[tauri::command]` + wires `start/stop/query/get_context` to the Rust core. +. **#9 — Delete legacy `android/` tree.** Both banned-language checks now + green. Behaviour parity verified by running APK on hardware + Termux + CLI smoke test. + +PRs #2 and #3 are sequenced and blocking. PRs #4 through #7 can land in +parallel after #3. PR #8 needs #4 done. PR #9 is gated on all prior. + +== Open questions for owner + +. **Tauri vs Dioxus.** Policy lists both. RFC recommends **Tauri 2** for + reasons: (a) `gen/android/` shim pattern is documented and tested, + (b) AffineScript→ESM→webview is the cleanest UI fit, (c) Dioxus mobile + on Android (`cargo-mobile2`) is newer and has thinner foreground-service + precedent. Confirm or override. +. **CI exemption scope.** OK to exempt `src-tauri/gen/android/` from the + Java/Kotlin check? This is the linchpin — without this exemption the + migration cannot land cleanly because Android demands JVM bytecode at + three system boundaries (Service / BroadcastReceiver / AppWidgetProvider). +. **Estate governance check.** Who owns the + `governance / Language / package anti-pattern policy` callable in + `hyperpolymath/standards`? Cross-repo PR needed before #2. +. **Widget config sacrifice.** Acceptable to drop the widget-configure + activity (Path B partial) in exchange for a smaller JVM surface? Saves + one shim. +. **Behaviour-parity verification.** Hardware test plan: Oppo Reno 13 + (current target per `app/build.gradle.kts:21`)? Or use Android emulator + + Termux on a generic device? +. **SPDX header policy clash.** Existing Kotlin + AndroidManifest.xml use + `PMPL-1.0-or-later`; in-repo `hooks/validate-spdx.sh` matches PMPL; but + the local `.git/hooks/pre-commit` still expects `MPL-2.0`. This RFC + uses MPL-2.0 to land cleanly under the active local hook, but new + sub-PRs will need a resolved policy. Recommend updating + `.git/hooks/pre-commit` to recognize neurophone as a PMPL repo and + re-stamping the existing PMPL files if MPL is canonical, or the + reverse if PMPL is canonical. + +== Acceptance criteria + +* `Check for Banned Languages` job — green on `main`. +* `governance / Language / package anti-pattern policy` — green on `main`. +* `android/` directory — removed. +* APK builds via `cargo tauri android build`. +* Foreground service + boot-restart + widget + sensor capture all work on + hardware. +* Termux CLI smoke test passes (`neurophone` command — unchanged by this + migration, but verified not regressed). + +== Non-goals + +* No changes to the seven core Rust crates (`lsm`, `esn`, `bridge`, + `sensors`, `llm`, `claude-client`, `neurophone-core`) outside what JNI + wiring strictly requires. +* No changes to the Termux CLI path. +* No new dependencies beyond `tauri`, `tauri-build`, and the + `android-activity` / `ndk` ecosystem crates Tauri already pulls in. From c8adb2e8c004f4429d63b695d8c7a428ceeded44 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:43:33 +0100 Subject: [PATCH 2/4] =?UTF-8?q?docs(migration):=20pivot=20RFC=20Tauri=20?= =?UTF-8?q?=E2=86=92=20Gossamer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner correction: estate target is Gossamer (Hyperpolymath's Zig+Ephapax+ webview framework), not Tauri. Rewrites the RFC accordingly: - Component mapping now references GossamerActivity / GossamerBridge from hyperpolymath/gossamer (MPL-2.0), not Tauri 2. - Sub-PR sequence rewritten: gossamer-rs crate + gossamer.conf.json scaffolding (panll precedent at src-gossamer/) rather than `cargo tauri android init`. - New "Upstream-vs-downstream scope" section. Gossamer Phase 3 (Mobile) is webview-only upstream; foreground service, AppWidgetProvider, BroadcastReceiver, SensorManager integration have no Gossamer support today and no open Gossamer issues. RFC recommends building all seven downstream in neurophone's android/ tree as application-specific Java shims, not blocking on Gossamer upstream PRs. - SPDX clash section removed; everything is MPL-2.0 per owner. - Gradle DSL question added (Groovy *.gradle vs Kotlin *.gradle.kts). Still draft, still no Kotlin deleted. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../RFC-ANDROID-KOTLIN-TO-RUST.adoc | 293 ++++++++++-------- 1 file changed, 159 insertions(+), 134 deletions(-) diff --git a/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc index 619a618..ac0d81f 100644 --- a/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc +++ b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc @@ -1,11 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// NOTE: Task instruction asked for PMPL-1.0-or-later, but the active -// local pre-commit hook (.git/hooks/pre-commit) requires MPL-2.0 for -// neurophone. The repo's existing Kotlin/manifest files use PMPL, so -// the policies disagree. MPL chosen here to land the RFC; see -// "Open questions for owner" for resolution. -= RFC: Android Kotlin → Rust/Tauri Migration += RFC: Android Kotlin → Rust/Gossamer Migration :toc: :toclevels: 3 :revdate: 2026-06-02 @@ -31,20 +26,38 @@ android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetConfigureActivity This keeps two estate CI gates permanently red: -* `Check for Banned Languages` (workflow `language-policy.yml`, step - `Check for Java/Kotlin files`) -* `governance / Language / package anti-pattern policy` +* `Check for Banned Languages` (`.github/workflows/language-policy.yml:87-94`) +* `governance / Language / package anti-pattern policy` (estate-wide reusable) -Both are sourced from `.github/workflows/language-policy.yml:87-94` which -hard-fails on any `*.kt` / `*.kts` / `*.java` match. +Both hard-fail on any `*.kt` / `*.kts` / `*.java` match. == Goal -* Migrate the Android UI + service to Tauri 2.0 (Rust backend + AffineScript/web UI). +* Migrate the Android UI + service to **Gossamer** (Hyperpolymath's + estate-canonical mobile framework: Zig FFI + Ephapax linear types + + webview shell, replaces Tauri across the estate). * Both CI checks turn green. * Behaviour preserved: sensor capture, foreground presence, widgets, boot-start. -* Sub-PRs are small and reviewable; nothing bulk-deleted until owner approves - this RFC. +* Sub-PRs are small and reviewable; nothing bulk-deleted until owner + approves this RFC. + +== Estate context + +* **Gossamer** lives at `hyperpolymath/gossamer`. Webview shell on Android + via `GossamerActivity` (`android/src/main/java/io/gossamer/GossamerActivity.java`, + MPL-2.0) which loads `libgossamer.so` and bridges JS↔native via + `GossamerBridge`. +* **Precedent**: `panll` migrated `src-tauri/` → `src-gossamer/`; 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. **Desktop only** — panll did not migrate a + foreground service or home-screen widget; this neurophone migration is + the first estate consumer to exercise that surface. +* **Gossamer Android maturity**: ROADMAP Phase 3 ("Mobile") lists Android + as *Partially Implemented*. Only `GossamerActivity` + `GossamerBridge` + exist upstream today. Foreground service, `AppWidgetProvider`, + `BroadcastReceiver`, `SensorManager` integration — **none exist + upstream**. No open Gossamer GH issues file these gaps. == Component inventory + replacement mapping @@ -54,78 +67,75 @@ hard-fails on any `*.kt` / `*.kts` / `*.java` match. | `MainActivity.kt` | 377 -| UI shell (status / input / response), sensor capture in-activity, permissions, - intent handling (MAIN, SEND, VIEW `neurophone://`, ASSIST, widget query). -| **Tauri 2** activity (`gen/android/`-scaffolded `MainActivity` extends - `TauriActivity`). UI markup authored in **AffineScript** compiling to - Deno-ESM, loaded by Tauri's webview. Rust commands expose - `init/start/stop/query/get_neural_context` via `#[tauri::command]`. - Sensor capture moves out of activity → service. +| UI shell (status / input / response), sensor capture in-activity, + permissions, intent handling (MAIN, SEND, VIEW `neurophone://`, ASSIST, + widget query). +| **`NeurophoneMainActivity extends GossamerActivity`** — Java subclass + (MPL-2.0) under `android/src/main/java/ai/neurophone/`. Overrides + `getInitialUrl()` to load the AffineScript-compiled UI bundle. Sensor + capture moves out of activity → service. Intent handling + (`onNewIntent`) dispatches into Rust via the existing `GossamerBridge` + JNI surface. | `NeurophoneService.kt` | 162 | `Service` subclass: wake-lock, accelerometer @ 50 Hz, salience window, `Notification.Builder` foreground channel, 1 Hz widget tick, calls `NativeLib.init/start/stop/pushSensorEvent/getNeuralContext`. -| **Rust foreground service via thin Java entry-point**. Service class - `NeurophoneRuntimeService extends Service` is auto-generated at build time - under `gen/android/app/src/main/java/` (Tauri convention — generated - Java, not hand-written Kotlin). Lifecycle hooks (`onCreate / onStartCommand - / onDestroy / onSensorChanged`) immediately `JNI_OnLoad` into Rust - fns in `crates/neurophone-android`. Salience window, sensor processing, - wake-lock release, and widget publishing all in Rust. +| **`NeurophoneRuntimeService extends Service`** — hand-written Java shim + under `android/src/main/java/ai/neurophone/` (MPL-2.0). + `onCreate / onStartCommand / onDestroy / onSensorChanged` each one-line + bodies into Rust JNI fns in `crates/neurophone-android`. Salience + window, sensor processing, wake-lock logic — all Rust. | `BootReceiver.kt` | 25 -| `BroadcastReceiver` for `BOOT_COMPLETED` + `LOCKED_BOOT_COMPLETED`; starts - service if widget was running pre-reboot. -| **Generated Java `BootReceiver extends BroadcastReceiver`** under - `gen/android/`. Single `onReceive` body: JNI into Rust +| `BroadcastReceiver` for `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED`; + starts service if widget was running pre-reboot. +| **`NeurophoneBootReceiver extends BroadcastReceiver`** — minimal Java + shim. Single `onReceive` body: JNI into Rust `crates/neurophone-android::boot_receive`. | `NativeLib.kt` | 93 | JNI surface declarations + sensor-type-string→int mapping. -| **DELETE entirely.** All JNI exports already live (as stubs) in - `crates/neurophone-android/src/lib.rs`. Sensor mapping moves to - Rust. +| **DELETE entirely.** All JNI exports already stubbed in + `crates/neurophone-android/src/lib.rs`. Fill the stubs; sensor mapping + moves to Rust. | `NeurophoneAppWidget.kt` | 152 | `AppWidgetProvider` (home-screen widget); renders title/state/salience progress bar/Ask button via `RemoteViews`; persists widget state to `SharedPreferences`. -| **Generated Java `NeurophoneAppWidget extends AppWidgetProvider`** under - `gen/android/`. `onUpdate / onReceive` each contain ONE JNI call into - Rust. `RemoteViews` construction stays in Java (it has to — `RemoteViews` - cannot be assembled from JNI without round-tripping every setter), but - the *decision* of what to render (running flag, salience value, - description string) comes from Rust. Prefs storage moves to Rust via - `ndk-context`-mediated JNI calls or a Rust-side SQLite cache. +| **`NeurophoneAppWidget extends AppWidgetProvider`** — Java shim. + `onUpdate / onReceive` each one JNI call into Rust. `RemoteViews` + setters stay in Java (cannot be assembled cleanly from JNI), but + which strings / which progress value / which intents come from Rust + via a single data-bearing JNI fn. | `NeurophoneWidgetActions.kt` | 47 | Lightweight `BroadcastReceiver` dispatching `PUBLISH_STATE` / `FORCE_REFRESH` from non-widget callers. -| **Generated Java `NeurophoneWidgetActions extends BroadcastReceiver`**; +| **`NeurophoneWidgetActions extends BroadcastReceiver`** — Java shim, body JNIs into Rust dispatch fn. | `NeurophoneWidgetConfigureActivity.kt` | 70 | Widget config UI (one checkbox: local-only mode), saved to prefs. -| **Generated Java `NeurophoneWidgetConfigureActivity extends Activity`** - with programmatic layout. Saving local-only-mode goes through JNI to - Rust config storage. Acceptable alternative: drop the widget config - entirely — a single setting like "local-only" can move to the main - Tauri UI and a sensible default applied at widget install. +| **`NeurophoneWidgetConfigureActivity extends Activity`** — Java shim + with programmatic layout. Saving goes through JNI to Rust config + storage. Acceptable alternative: drop the widget config entirely and + surface "local-only" in the main Gossamer UI; saves one shim. | `*.gradle.kts` (×3) | ~100 -| Project + app module build config. -| **Replaced by Tauri-generated `gen/android/**/*.gradle.kts`** at - `cargo tauri android init` time. These are platform-prescribed by - Android Gradle Plugin: there is no Rust replacement and Tauri vends - them. The CI exemption section below covers this. +| Android Gradle Plugin build config. +| **Converted to `*.gradle` (Groovy DSL) or kept as `*.gradle.kts` under + a CI exemption.** Gossamer's existing `android/` directory uses raw + Gradle (no Kotlin DSL in current upstream). Recommend matching upstream: + Groovy `*.gradle` files. Banned-language check already accepts these. |=== == The unavoidable JVM-bytecode surface @@ -136,123 +146,138 @@ demands a class implementing the listed Android framework superclass: * `Activity` (main + widget-configure) * `Service` -* `BroadcastReceiver` (boot + widget actions + widget provider) +* `BroadcastReceiver` (boot + widget actions) +* `AppWidgetProvider` (extends `BroadcastReceiver`) Three resolution paths exist; this RFC proposes **(A)** and explicitly *rejects* (B) and (C). Owner picks before sub-PR work begins. -=== Path (A) PROPOSED — Tauri-generated Java shims under `gen/android/` - -* `cargo tauri android init` generates per-platform Java glue inside - `src-tauri/gen/android/` (analogous to how `node_modules/` is generated - but not authored). -* These files are **not Kotlin**, they are minimal Java each delegating - immediately to a single JNI function in Rust. -* Add `gen/android/` to the `Check for Java/Kotlin files` step's exemption - list — same exemption pattern as `node_modules/` for the - `Check for ReScript files` step. -* The `governance / Language / package anti-pattern policy` check is - estate-wide (`hyperpolymath/standards`); adding the exemption needs a - one-line change in the reusable workflow caller. Pre-coordination - required. -* **Outcome:** zero hand-written Kotlin/Java in the repo's source tree; - CI passes; behaviour preserved. +=== Path (A) PROPOSED — hand-written **Java** shims under `android/` + +* All shims are Java (MPL-2.0), one file per Android system surface + (Service, BootReceiver, AppWidget, WidgetActions, WidgetConfigure, + MainActivity). +* Each shim is a **single class, single delegating method body** — typically + 3-10 lines — that immediately JNIs into Rust. No business logic in + Java. +* The estate-wide language-policy CI currently blocks all `*.java` and + `*.kt`. The Java check has to either (i) gain an exemption for + `android/**/*.java` *across the estate* via the reusable governance + workflow in `hyperpolymath/standards`, OR (ii) gain a neurophone-local + exemption only. Pre-PR coordination on which scope is acceptable. +* **Outcome:** zero Kotlin in the tree; small audited Java shim surface + (estimated < 250 LoC total across all shims combined, vs. current 926 + LoC of Kotlin); CI passes once exemption lands. === Path (B) REJECTED — drop the widget surface * Replace home-screen widget with a quick-settings tile + persistent - notification. Both can be declared in `AndroidManifest.xml` and driven - from a service whose only Java shim is auto-generated. -* Pro: smaller JVM surface. -* Con: regresses a shipped, user-visible feature; the OS integration - doc (`docs/OS_INTEGRATION.adoc`) lists the home-screen widget as the - primary integration point. + notification. +* Pro: smaller JVM shim surface (no AppWidgetProvider + no WidgetActions + + no WidgetConfigureActivity = 3 fewer shims). +* Con: regresses the user-visible home-screen widget feature. The + `docs/OS_INTEGRATION.adoc` doc lists the home-screen widget as the + primary Android integration surface. -=== Path (C) REJECTED — JVM bytecode generation from Rust +=== Path (C) REJECTED — JVM bytecode emission from Rust / Zig -* Use `j4rs` / `dx-java` / manual `.class` emission to ship JVM classes - built from Rust at compile time. -* Pro: no Java source at all. -* Con: experimental, fragile, no maintained tooling for `AppWidgetProvider` - superclass binding; not worth the risk. +* `j4rs` or hand-emitted `.class` files for the Service/Receiver/Provider + superclass bindings. +* Pro: zero Java in the source tree. +* Con: experimental, fragile, no maintained tooling for + `AppWidgetProvider`; not worth the risk. + +== Upstream-vs-downstream scope + +Per Gossamer ROADMAP Phase 3 (Mobile, partially implemented) — the seven +missing platform-integration capabilities (service, widget, receivers, +sensors, intent dispatch, prefs, deep-links) are **not** in Gossamer's +scope. Gossamer is a webview shell. + +**Recommendation:** build all seven downstream in neurophone's `android/` +tree as application-specific Java shims. Do not block neurophone's +migration on Gossamer upstream PRs. + +*Possible later upstream contribution:* if a second estate consumer +(idaptik-ums? a future mobile project?) needs the same surface, extract +a `gossamer-android-services` companion module then. Not for this RFC. == Sub-PR sequence Each is a small, reviewable PR. Numbers are PR ordering, not commit count. -. **#1 (THIS PR — DRAFT, no code changes)** — RFC for owner review. No - Kotlin deleted, no Tauri scaffolding generated. -. **#2 — CI exemption for generated mobile shims.** Update +. **#1 (THIS PR — DRAFT, no code changes)** — RFC for owner review. + No Kotlin deleted, no Gossamer scaffolding generated. +. **#2 — CI exemption for hand-written Java mobile shims.** Update `.github/workflows/language-policy.yml` step `Check for Java/Kotlin - files` to exempt `src-tauri/gen/android/`. Coordinated with - `hyperpolymath/standards` for the reusable governance check. **Lands - before any Tauri scaffolding so CI doesn't go redder en route.** -. **#3 — Tauri scaffolding (no behaviour).** Run `cargo tauri android - init`; commit `src-tauri/` + generated `gen/android/`. No deletion of - the existing `android/` Gradle project; both exist side by side so we - can A/B sanity-check. + files` to exempt `android/**/*.java`. Coordinated with + `hyperpolymath/standards` for the reusable governance check. + **Lands before any Gossamer scaffolding so CI doesn't go redder en route.** +. **#3 — Gossamer scaffolding (no behaviour).** Add `gossamer-rs` to the + Rust workspace; create `gossamer.conf.json`; add an `android/` Java + shim for `NeurophoneMainActivity extends GossamerActivity` loading a + placeholder HTML page. Keep the existing Kotlin `android/` directory + side-by-side so we can A/B sanity-check (rename to `android-legacy/` if + the build system can't tolerate both). . **#4 — Port `NativeLib` to Rust.** Fill out `crates/neurophone-android/src/lib.rs` with the 10 JNI exports the current Kotlin `NativeLib` declares. Wire to existing crates (`neurophone-core`, `sensors`, `lsm`, `esn`, `bridge`, `llm`, `claude-client`). Tests: JNI roundtrip via a Rust test that simulates `JNIEnv`. -. **#5 — Port `NeurophoneService` to Rust.** Java shim under `gen/android/` - delegates `onCreate / onStartCommand / onDestroy / onSensorChanged` to - Rust. Salience window, wake-lock plumbing, widget tick — all Rust. -. **#6 — Port `BootReceiver` to Rust.** Same shim pattern, single fn. -. **#7 — Port widget triple (provider / actions / configure) to Rust.** - Java shims under `gen/android/` are minimal; widget render logic - (selecting strings, salience-pct, what `RemoteViews` setters to call) - is decided in Rust and pushed back to the shim via a single +. **#5 — Port `NeurophoneService`.** Java shim + (`NeurophoneRuntimeService extends Service`) → JNI into Rust. Salience + window, wake-lock plumbing, widget tick — all Rust. +. **#6 — Port `BootReceiver`.** Java shim → single JNI fn. +. **#7 — Port widget triple** (provider / actions / configure). Java + shims; widget render decisions in Rust pushed back via one data-bearing JNI fn. -. **#8 — AffineScript UI** (replaces `activity_main.xml` view binding). - Tauri loads the AffineScript-compiled UI in webview; `#[tauri::command]` - wires `start/stop/query/get_context` to the Rust core. -. **#9 — Delete legacy `android/` tree.** Both banned-language checks now - green. Behaviour parity verified by running APK on hardware + Termux - CLI smoke test. +. **#8 — AffineScript UI** (replaces `activity_main.xml` view-binding + layout). AffineScript compiles to JS, bundles to + `android/src/main/assets/` (or wherever Gossamer loads from), loaded + in `GossamerActivity`. `gossamer_rs::App::command()` registrations + wire `start/stop/query/get_context` to the Rust core. +. **#9 — Delete legacy Kotlin `android/` tree.** Both banned-language + checks now green. Behaviour parity verified on hardware + Termux CLI + smoke test. PRs #2 and #3 are sequenced and blocking. PRs #4 through #7 can land in parallel after #3. PR #8 needs #4 done. PR #9 is gated on all prior. == Open questions for owner -. **Tauri vs Dioxus.** Policy lists both. RFC recommends **Tauri 2** for - reasons: (a) `gen/android/` shim pattern is documented and tested, - (b) AffineScript→ESM→webview is the cleanest UI fit, (c) Dioxus mobile - on Android (`cargo-mobile2`) is newer and has thinner foreground-service - precedent. Confirm or override. -. **CI exemption scope.** OK to exempt `src-tauri/gen/android/` from the - Java/Kotlin check? This is the linchpin — without this exemption the - migration cannot land cleanly because Android demands JVM bytecode at - three system boundaries (Service / BroadcastReceiver / AppWidgetProvider). +. **CI exemption scope.** Estate-wide exemption for `android/**/*.java` + via `hyperpolymath/standards`, or neurophone-local exemption only? + This is the linchpin — without it the migration cannot land cleanly + because Android demands JVM bytecode at four system boundaries + (`Activity` / `Service` / `BroadcastReceiver` / `AppWidgetProvider`). . **Estate governance check.** Who owns the `governance / Language / package anti-pattern policy` callable in `hyperpolymath/standards`? Cross-repo PR needed before #2. . **Widget config sacrifice.** Acceptable to drop the widget-configure - activity (Path B partial) in exchange for a smaller JVM surface? Saves - one shim. -. **Behaviour-parity verification.** Hardware test plan: Oppo Reno 13 - (current target per `app/build.gradle.kts:21`)? Or use Android emulator - + Termux on a generic device? -. **SPDX header policy clash.** Existing Kotlin + AndroidManifest.xml use - `PMPL-1.0-or-later`; in-repo `hooks/validate-spdx.sh` matches PMPL; but - the local `.git/hooks/pre-commit` still expects `MPL-2.0`. This RFC - uses MPL-2.0 to land cleanly under the active local hook, but new - sub-PRs will need a resolved policy. Recommend updating - `.git/hooks/pre-commit` to recognize neurophone as a PMPL repo and - re-stamping the existing PMPL files if MPL is canonical, or the - reverse if PMPL is canonical. + activity (Path B partial) in exchange for a smaller JVM surface? + Saves one shim. +. **Gradle DSL choice.** Match Gossamer upstream's Groovy `*.gradle`, or + keep Kotlin `*.gradle.kts` under a separate exemption? The CI check + blocks `*.kts`; using Groovy is the cleaner path. +. **Hardware test plan.** Oppo Reno 13 (current target per + `app/build.gradle.kts:21`) or Android emulator + Termux on a generic + device? +. **Upstream contribution.** Build the four shim classes purely in + neurophone, or factor a `gossamer-android-services` companion as we + go? RFC recommends neurophone-local for now; revisit when a second + consumer appears. == Acceptance criteria * `Check for Banned Languages` job — green on `main`. * `governance / Language / package anti-pattern policy` — green on `main`. -* `android/` directory — removed. -* APK builds via `cargo tauri android build`. -* Foreground service + boot-restart + widget + sensor capture all work on - hardware. +* `android/` directory — contains only Java shims + Groovy Gradle + XML + resources; no `*.kt` / `*.kts` files. +* APK builds via the Gossamer Android build command (per Gossamer Phase 3 + conventions). +* Foreground service + boot-restart + widget + sensor capture all work + on hardware. * Termux CLI smoke test passes (`neurophone` command — unchanged by this migration, but verified not regressed). @@ -262,5 +287,5 @@ parallel after #3. PR #8 needs #4 done. PR #9 is gated on all prior. `sensors`, `llm`, `claude-client`, `neurophone-core`) outside what JNI wiring strictly requires. * No changes to the Termux CLI path. -* No new dependencies beyond `tauri`, `tauri-build`, and the - `android-activity` / `ndk` ecosystem crates Tauri already pulls in. +* No upstream Gossamer changes in this neurophone migration. Gossamer + mobile Phase 3 advances on its own track. From 944f94f705c1e96601d5b67950b353c31c95a52b Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:37:41 +0100 Subject: [PATCH 3/4] =?UTF-8?q?docs(migration):=20JNI=20surface=20audit=20?= =?UTF-8?q?=E2=80=94=20head-start=20for=20sub-PR=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maps every external fun in NativeLib.kt to (a) the Rust JNI export we'll write in crates/neurophone-android/src/lib.rs and (b) the underlying neurophone-core API it composes. Surfaces 5 gaps: 1. No start/stop/is_running lifecycle on NeuroSymbolicSystem → keep as JNI-layer concept (bool in static state holder). 2. Sensor-type int→string mapping currently in Kotlin → move into Rust JNI layer as const lookup. 3. query() uses heuristic prefer_local; Kotlin queryLocal/queryClaude expect hard routing → introduce QueryRoute enum on core. 4. No get_neural_context() on core → add method composing from get_state(). 5. No reset() on core → JNI layer replaces held instance. Estimated sub-PR #4 size: ~250 LoC new Rust + ~10 LoC core change + JNI roundtrip tests. Shovel-ready when sub-PRs #2 (CI exemption) + #3 (Gossamer scaffolding) land. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/migrations/JNI-SURFACE-AUDIT.adoc | 225 +++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 docs/migrations/JNI-SURFACE-AUDIT.adoc diff --git a/docs/migrations/JNI-SURFACE-AUDIT.adoc b/docs/migrations/JNI-SURFACE-AUDIT.adoc new file mode 100644 index 0000000..0ebe530 --- /dev/null +++ b/docs/migrations/JNI-SURFACE-AUDIT.adoc @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += JNI Surface Audit: `NativeLib.kt` ↔ `crates/neurophone-android` +:toc: +:toclevels: 2 +:revdate: 2026-06-02 +:status: Head-start audit for sub-PR #4 of [[RFC-ANDROID-KOTLIN-TO-RUST]] + +Companion to `RFC-ANDROID-KOTLIN-TO-RUST.adoc`. Maps every `external fun` +in `android/app/src/main/java/ai/neurophone/NativeLib.kt` to (a) the +Rust function we'll export from `crates/neurophone-android/src/lib.rs` +and (b) the underlying `neurophone-core` API it composes. Surfaces +gaps that need new methods on `NeuroSymbolicSystem` before sub-PR #4 +can finish. + +== Current state + +* `crates/neurophone-android/src/lib.rs` = 6-line stub (`pub fn hello() -> &'static str`). No JNI exports yet. +* `crates/neurophone-android/Cargo.toml` already declares `crate-type = + ["cdylib"]`, depends on `jni`, `neurophone-core`, `sensors`, + `serde_json`, `tokio`, `chrono`. Foundation is correct. +* `neurophone-core::NeuroSymbolicSystem` has 9 public methods. Coverage + vs the Kotlin JNI surface is partial (table below). + +== The mapping table + +[cols="2,3,3,2", options="header"] +|=== +| Kotlin `external fun` | JNI export (Rust) | Underlying `neurophone-core` | Gap + +| `init(configJson: String?): Boolean` +| `Java_ai_neurophone_NativeLib_init` +| `serde_json::from_str::(json)` → + `NeuroSymbolicSystem::new(cfg)?.initialize()?` +| **None** — core supports this directly. JNI layer holds the system in + a `OnceLock>>`. + +| `start(): Boolean` +| `Java_ai_neurophone_NativeLib_start` +| **MISSING on core.** Need `NeuroSymbolicSystem::start(&mut self)`. +| **Gap 1**: core has no `start`/`stop`/lifecycle methods. Either add + them to `neurophone-core` or let "running" be a JNI-layer concept + (a `bool` flag in the static state holder). RFC recommends the + latter to keep `neurophone-core` ABI lean. + +| `stop()` +| `Java_ai_neurophone_NativeLib_stop` +| Companion to `start` — same gap. +| **Gap 1** (same). + +| `processSensor(sensorType: Int, values: FloatArray, timestamp: Long, accuracy: Int): Boolean` +| `Java_ai_neurophone_NativeLib_processSensor` +| `NeuroSymbolicSystem::process_sensor_event(SensorEvent { sensor_type, timestamp_ms, values })` +| **Gap 2**: sensor-type int→string mapping. Kotlin's `NativeLib.pushSensorEvent` + wrapper maps `1 → "accelerometer"`, `4 → "gyroscope"`, `2 → "magnetometer"`, + `5 → "light"`, `8 → "proximity"`. This mapping moves into the Rust JNI + layer as a `const ID_TO_NAME: &[(jint, &str)]` lookup. + `accuracy` argument in the Kotlin signature is currently unused by + `process_sensor_event` — either drop it from the JNI signature (breaks + Kotlin ABI) or accept and ignore (preserves ABI; recommended). + +| `queryLocal(message: String): String` +| `Java_ai_neurophone_NativeLib_queryLocal` +| `NeuroSymbolicSystem::query(message, prefer_local=true, force_local=true)` +| **Gap 3**: `query` signature. Current core + `query()` exists at line 206 of `neurophone-core/src/lib.rs` but its + exact signature isn't visible from the public-surface grep — needs + verification. Likely needs a `force_local` flag added. + +| `queryClaude(message: String): String` +| `Java_ai_neurophone_NativeLib_queryClaude` +| `NeuroSymbolicSystem::query(message, prefer_local=false, force_cloud=true)` +| **Gap 3** (same — force_cloud flag). + +| `query(message: String, preferLocal: Boolean): String` +| `Java_ai_neurophone_NativeLib_query` +| `NeuroSymbolicSystem::query(message, prefer_local)` +| Should match core directly if the existing signature is + `query(&mut self, message: &str, prefer_local: bool)`. Verify. + +| `getNeuralContext(): String` +| `Java_ai_neurophone_NativeLib_getNeuralContext` +| **MISSING on core.** No `get_neural_context()` method. +| **Gap 4**: `neurophone-core` exposes `get_state()` (returns + `SystemState` struct) but not a stringified neural-context summary. + The Kotlin service expects the format + `"[NEURAL_STATE] salience=N.NN (synthetic) [/NEURAL_STATE]"` + (per `NeurophoneService.kt:113`). Need a new method on + `NeuroSymbolicSystem` or compose in the JNI layer from `get_state()`. + +| `getState(): String` +| `Java_ai_neurophone_NativeLib_getState` +| `serde_json::to_string(&system.get_state())` +| **None** — `SystemState` already `Serialize` (line 78 of + `neurophone-core/src/lib.rs`). JNI layer just calls + `serde_json::to_string`. + +| `reset()` +| `Java_ai_neurophone_NativeLib_reset` +| **MISSING on core.** No `reset()` method. +| **Gap 5**: requires either (a) replace the held instance with a + fresh `NeuroSymbolicSystem::new(config)` or (b) add a + `reset(&mut self)` method to core that re-initialises LSM/ESN/Bridge + state in-place without losing the config. (a) is simpler. + +| `isRunning(): Boolean` +| `Java_ai_neurophone_NativeLib_isRunning` +| Reads the JNI-layer "running" flag from **Gap 1**. +| Resolves via Gap 1. +|=== + +== Summary of gaps + +[cols="1,3,3", options="header"] +|=== +| # | Gap | Resolution + +| 1 | No `start`/`stop`/`is_running` lifecycle on `NeuroSymbolicSystem`. + | Keep as JNI-layer concept: a `bool` in the static state holder. + Do NOT add lifecycle methods to `neurophone-core` (keeps the crate + framework-agnostic). Sub-PR #4 includes this in + `crates/neurophone-android/src/state.rs`. + +| 2 | Sensor-type int→string mapping currently in Kotlin + (`NativeLib.kt:83-90`). + | Move into Rust JNI layer as a `const SENSOR_TYPE_NAMES: &[(i32, &str)]` + lookup. Same 5 mappings (`1→accelerometer`, `4→gyroscope`, + `2→magnetometer`, `5→light`, `8→proximity`). + +| 3 | `query` signature **VERIFIED** as + `query(&mut self, message: &str, prefer_local: bool) -> Result` + (`neurophone-core/src/lib.rs:206`). The Kotlin `queryLocal` / `queryClaude` callers + expect HARD routing; current `prefer_local` is a heuristic flag combined with a + word-count complexity threshold (`should_use_local = prefer_local && complexity < local_threshold`). + | Recommend: introduce + `enum QueryRoute { Auto, ForceLocal, ForceCloud }` and refactor `query` + to take it. `queryLocal` → `QueryRoute::ForceLocal`, `queryClaude` → + `QueryRoute::ForceCloud`, `query(..., prefer_local)` → + `if prefer_local { QueryRoute::Auto } else { QueryRoute::Auto }` with + the prefer flag passed through. Single small refactor to `neurophone-core`. + Alternative if owner wants zero core change: implement `queryLocal` / + `queryClaude` purely in the JNI layer by toggling + `system.config.local_threshold` before each call (1.0 = always local, + 0.0 = always cloud) then restoring — hacky but ABI-stable. + +| 4 | No `get_neural_context()` on `NeuroSymbolicSystem`. + | Add method that returns + `format!("[NEURAL_STATE] salience={:.2} (synthetic) [/NEURAL_STATE]", state.salience)` + — i.e. compose from `get_state()` in core. Keeps the format + contract owned by Rust, not the Kotlin shim. + +| 5 | No `reset()` on `NeuroSymbolicSystem`. + | JNI-layer approach: replace the held instance via + `*system_guard = Some(NeuroSymbolicSystem::new(saved_config)?)`. No + core change required if we also stash the original `SystemConfig` + in the static state holder. +|=== + +== Suggested file layout for sub-PR #4 + +---- +crates/neurophone-android/src/ +├── lib.rs -- #[no_mangle] JNI exports only, one per Java method +├── state.rs -- OnceLock>> + RuntimeState +├── sensor_map.rs -- ID_TO_NAME table + name_from_id() helper +└── error.rs -- JniError → jboolean / JString conversions +---- + +`state.rs::RuntimeState`: + +[source,rust] +---- +pub struct RuntimeState { + pub system: NeuroSymbolicSystem, + pub config: SystemConfig, // for reset() + pub running: bool, // for start()/stop()/isRunning() +} +---- + +== JNI signature mapping cheat-sheet + +[cols="2,3", options="header"] +|=== +| Kotlin `external fun` | Rust `#[no_mangle] pub extern "system"` + +| `init(configJson: String?): Boolean` +| `Java_ai_neurophone_NativeLib_init(env: JNIEnv, _cls: JClass, json: JString) -> jboolean` + +| `start(): Boolean` +| `Java_ai_neurophone_NativeLib_start(env: JNIEnv, _cls: JClass) -> jboolean` + +| `stop()` +| `Java_ai_neurophone_NativeLib_stop(env: JNIEnv, _cls: JClass)` + +| `processSensor(Int, FloatArray, Long, Int): Boolean` +| `Java_ai_neurophone_NativeLib_processSensor(env: JNIEnv, _cls: JClass, sensor_type: jint, values: jfloatArray, timestamp: jlong, accuracy: jint) -> jboolean` + +| `query(String, Boolean): String` +| `Java_ai_neurophone_NativeLib_query(env: JNIEnv, _cls: JClass, message: JString, prefer_local: jboolean) -> jstring` + +| `getNeuralContext(): String` / `getState(): String` +| `Java_ai_neurophone_NativeLib_getNeuralContext(env: JNIEnv, _cls: JClass) -> jstring` +|=== + +== Estimated size of sub-PR #4 + +* `crates/neurophone-android/src/lib.rs` — ~150 LoC (11 JNI exports × ~10-15 LoC each, mostly env↔Rust string conversion + lock acquisition + error mapping). +* `crates/neurophone-android/src/state.rs` — ~40 LoC. +* `crates/neurophone-android/src/sensor_map.rs` — ~25 LoC. +* `crates/neurophone-android/src/error.rs` — ~30 LoC. +* New methods on `neurophone-core`: `get_neural_context()` (~10 LoC). +* Tests: at least one JNI roundtrip test simulating `JNIEnv` for each + string-returning fn. + +**Total estimate**: ~250 LoC new Rust + ~10 LoC change to +`neurophone-core` + tests. Manageable single PR. + +== What this audit does NOT block + +* Sub-PR #4 cannot start until sub-PRs #2 (CI exemption) and #3 + (Gossamer scaffolding) land, per the RFC sequence. This audit is a + head-start so #4 is shovel-ready when its turn comes. +* `NeuroSymbolicSystem::query` signature verified — see Gap 3 above. +* No outstanding research items before sub-PR #4 can begin (apart from + it being gated on #2 + #3 per the RFC). From 4c3ae0fd57ac7587f1ce68e4a9a8cf4ec3e8e8b5 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:41:25 +0100 Subject: [PATCH 4/4] docs(migration): record owner decisions Q1-Q6 (2026-06-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q1: estate-wide CI exemption (standards#341). Q2: self-answered. Q3: drop widget-configure activity. Q4: Groovy *.gradle. Q5: emulator+Termux for dev, Oppo Reno 13 for final close-out. Q6: contribute upstream — `gossamer-android-services` companion in hyperpolymath/gossamer with four shim base classes (Service, BroadcastReceiver, AppWidgetProvider, Activity-with-bridge). Q6 changes the sub-PR sequence: a new #2b — Gossamer companion PR — blocks the original #3 (neurophone Gossamer scaffolding). Revised 9-step sequence captured in the new "Sequencing implications of Q6" section. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../RFC-ANDROID-KOTLIN-TO-RUST.adoc | 117 ++++++++++++------ 1 file changed, 80 insertions(+), 37 deletions(-) diff --git a/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc index ac0d81f..bbddce1 100644 --- a/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc +++ b/docs/migrations/RFC-ANDROID-KOTLIN-TO-RUST.adoc @@ -4,7 +4,7 @@ :toc: :toclevels: 3 :revdate: 2026-06-02 -:status: DRAFT — awaiting owner review BEFORE any bulk conversion +:status: Q1-Q6 RESOLVED 2026-06-02; sub-PR #2 in flight (neurophone#107 + standards#341) == Problem @@ -187,20 +187,29 @@ Three resolution paths exist; this RFC proposes **(A)** and explicitly * Con: experimental, fragile, no maintained tooling for `AppWidgetProvider`; not worth the risk. -== Upstream-vs-downstream scope +== Upstream-vs-downstream scope (RESOLVED — UPSTREAM) Per Gossamer ROADMAP Phase 3 (Mobile, partially implemented) — the seven missing platform-integration capabilities (service, widget, receivers, -sensors, intent dispatch, prefs, deep-links) are **not** in Gossamer's -scope. Gossamer is a webview shell. - -**Recommendation:** build all seven downstream in neurophone's `android/` -tree as application-specific Java shims. Do not block neurophone's -migration on Gossamer upstream PRs. - -*Possible later upstream contribution:* if a second estate consumer -(idaptik-ums? a future mobile project?) needs the same surface, extract -a `gossamer-android-services` companion module then. Not for this RFC. +sensors, intent dispatch, prefs, deep-links) are not in Gossamer's +current scope. Gossamer is today a webview shell. + +**Owner decision 2026-06-02 (Q6):** contribute the missing surface +**upstream** to `hyperpolymath/gossamer` as a `gossamer-android-services` +companion module. The four shim base classes (Service, BroadcastReceiver, +AppWidgetProvider, Activity-with-bridge) land in Gossamer upstream so +future estate Android consumers inherit them. + +**Implications:** + +* Sub-PR #3 (neurophone Gossamer scaffolding) blocks on the Gossamer + upstream PR landing. +* The Gossamer companion API shape needs owner review — generic enough + for future consumers (idaptik-ums Android port, etc.), not just + neurophone-shaped. +* neurophone-side shims (Service / BootReceiver / Widget triple) become + thin subclasses extending the Gossamer base classes rather than + Android framework classes directly. == Sub-PR sequence @@ -238,35 +247,69 @@ Each is a small, reviewable PR. Numbers are PR ordering, not commit count. in `GossamerActivity`. `gossamer_rs::App::command()` registrations wire `start/stop/query/get_context` to the Rust core. . **#9 — Delete legacy Kotlin `android/` tree.** Both banned-language - checks now green. Behaviour parity verified on hardware + Termux CLI - smoke test. + checks now green. Behaviour parity verified on emulator AND Oppo Reno + 13 hardware (per Q5 — emulator for development, hardware for the + final close-out) + Termux CLI smoke test. PRs #2 and #3 are sequenced and blocking. PRs #4 through #7 can land in parallel after #3. PR #8 needs #4 done. PR #9 is gated on all prior. -== Open questions for owner - -. **CI exemption scope.** Estate-wide exemption for `android/**/*.java` - via `hyperpolymath/standards`, or neurophone-local exemption only? - This is the linchpin — without it the migration cannot land cleanly - because Android demands JVM bytecode at four system boundaries - (`Activity` / `Service` / `BroadcastReceiver` / `AppWidgetProvider`). -. **Estate governance check.** Who owns the - `governance / Language / package anti-pattern policy` callable in - `hyperpolymath/standards`? Cross-repo PR needed before #2. -. **Widget config sacrifice.** Acceptable to drop the widget-configure - activity (Path B partial) in exchange for a smaller JVM surface? - Saves one shim. -. **Gradle DSL choice.** Match Gossamer upstream's Groovy `*.gradle`, or - keep Kotlin `*.gradle.kts` under a separate exemption? The CI check - blocks `*.kts`; using Groovy is the cleaner path. -. **Hardware test plan.** Oppo Reno 13 (current target per - `app/build.gradle.kts:21`) or Android emulator + Termux on a generic - device? -. **Upstream contribution.** Build the four shim classes purely in - neurophone, or factor a `gossamer-android-services` companion as we - go? RFC recommends neurophone-local for now; revisit when a second - consumer appears. +**Note**: the section above is the *original* sequence. The +**revised sequence per Q6** lives in `Sequencing implications of Q6` +below — read that for the post-2026-06-02 plan. + +== Owner decisions (all resolved 2026-06-02) + +[cols="1,5,3", options="header"] +|=== +| # | Question | Resolution + +| Q1 | **CI exemption scope** — estate-wide via `hyperpolymath/standards` or neurophone-local? +| **Estate-wide.** Filed as `hyperpolymath/standards#341` (open). Glob `(^|/)android/.*/src/.*\.java$`; Kotlin (.kt, .kts) stays banned. neurophone auto-consumes via `@main` pin on the reusable. + +| Q2 | **Standards reusable owner** — who reviews the cross-repo PR? +| Self-answered. Owner approves directly; no third-party gatekeeper. + +| Q3 | **Widget-configure activity** — keep or drop? +| **DROP.** The single setting (local-only mode) moves into the main Gossamer UI; widget gets sensible default at install. Saves one Java shim (`NeurophoneWidgetConfigureActivity`). + +| Q4 | **Gradle DSL** — Groovy `*.gradle` or Kotlin `*.gradle.kts`? +| **Groovy `*.gradle`.** Matches Gossamer upstream's `android/` tree. Banned-language check accepts Groovy out of the box. Single estate convention. + +| Q5 | **Hardware test plan** — Oppo Reno 13 or emulator + Termux? +| **Both.** Emulator + Termux smoke test during sub-PR development (faster iteration); Oppo Reno 13 hardware verification before sub-PR #9 deletes the legacy Kotlin tree. + +| Q6 | **Shim location** — neurophone-local or factor `gossamer-android-services` upstream? +| **Contribute upstream NOW.** Build the four shim base classes (`Service` / `BroadcastReceiver` / `AppWidgetProvider` / `Activity`) as a `gossamer-android-services` companion in `hyperpolymath/gossamer`. neurophone consumes them. **Coupled ordering**: sub-PR #3 BLOCKS on the Gossamer upstream companion landing. +|=== + +== Sequencing implications of Q6 + +Q6's "contribute upstream now" choice **changes the sub-PR sequence**: + +. The original sub-PR #3 (Gossamer scaffolding in neurophone) now depends + on a NEW prerequisite: **Gossamer companion PR** in + `hyperpolymath/gossamer` adding `gossamer-android-services` with the + four shim base classes. +. The Gossamer companion design needs owner review separately — the API + shape for `GossamerForegroundService`, `GossamerAppWidgetProvider`, + `GossamerBootReceiver`, etc. has to be generic enough for future + estate Android consumers (not just neurophone). +. neurophone's sub-PRs #5/#6/#7 (the Service/BootReceiver/Widget ports) + become consumers of the Gossamer companion — they `extend` the + upstream base classes rather than `extend android.app.Service` directly. + +Revised sequence: + +. **#1 (THIS PR)** — RFC, no code changes. +. **#2a** — CI exemption (standards#341, neurophone#107). **IN FLIGHT.** +. **#2b — NEW** — Gossamer companion PR: `gossamer-android-services` + module with four shim base classes + tests. Owner reviews API shape. +. **#3** — Gossamer scaffolding in neurophone (depends on #2b). +. **#4** — Port NativeLib to Rust (parallel-safe). +. **#5/#6/#7** — Port Service / BootReceiver / Widget triple (depends on #2b + #3). +. **#8** — AffineScript UI. +. **#9** — Delete legacy Kotlin `android/`. == Acceptance criteria