@ondeinference / ed / commits / 7cdb1d7

chore: remove skills unrelated to onde-ed workspace

Drop humanizer, sdk-dart, sdk-kotlin, sdk-react-native — none of these apply to a pure Rust workspace. Remaining skills all touch code that lives in this repo (UniFFI, Swift package, Rust standards, releases, regressions, forked deps, legal, branding). Co-Authored-By: siGit Code v1.6.6-acp <noreply@sigit.si>

Seto Elkahfi committed Oct 10, 2026 at 08:19 UTC 7cdb1d7b056140d465c9e3d9d65d1ea6b1e6bc72
4 files changed -3014
.agents/skills/humanizer/SKILL.md deleted
-431
@@ -1,431 +0,0 @@
1 ----
2 -name: humanizer
3 -description: Remove signs of AI-generated writing from text. Use after drafting to make copy sound more natural and human-written. Based on Wikipedia's "Signs of AI writing" guide.
4 -allowed-tools: Read, Write, Edit, Glob, Grep, AskUserQuestion
5 -user-invocable: true
6 ----
7 -
8 -# Humanizer: Remove AI Writing Patterns
9 -
10 -You are a writing editor that identifies and removes signs of AI-generated text. This guide is based on Wikipedia's "Signs of AI writing" page, maintained by WikiProject AI Cleanup.
11 -
12 -Key insight: "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases."
13 -
14 -## Invocation
15 -
16 -```bash
17 -/humanizer # Review text for AI patterns
18 -/humanizer "paste text here" # Humanize specific text
19 -```
20 -
21 -## Your Task
22 -
23 -When given text to humanize:
24 -
25 -1. **Identify AI patterns** - Scan for the 24 patterns listed below
26 -2. **Rewrite problematic sections** - Replace AI-isms with natural alternatives
27 -3. **Preserve meaning** - Keep the core message intact
28 -4. **Protect exact names** - Do not change brand casing, package names, commands, file paths, JSON keys, or code just to make a sentence feel smoother
29 -5. **Add soul** - Don't just remove bad patterns; inject actual personality
30 -6. **Final audit pass** - Ask "What makes this obviously AI generated?" then revise again
31 -
32 ----
33 -
34 -## BRAND AND NAMING GUARDRAILS
35 -
36 -Humanizing copy does **not** give you permission to rename things.
37 -
38 -Preserve exact casing and spelling for:
39 -
40 -- product and company names
41 -- package names
42 -- CLI commands
43 -- protocol names and acronyms
44 -- JSON keys, environment variables, paths, and code identifiers
45 -
46 -If a sentence feels stiff because a brand name is rigid, rewrite the sentence around the name. Do not "fix" the name.
47 -
48 -Examples:
49 -
50 -- Keep `siGit Code` as `siGit Code`, not `siGit`, `Sigit Code`, or `sigit Code`
51 -- Keep `smbCloud` as `smbCloud`
52 -- Keep `Onde Inference` as `Onde Inference`
53 -- Keep `sigit`, `@smbcloud/sigit`, `sigit-code`, and `onde` exactly as written when they refer to commands or packages
54 -
55 ----
56 -
57 -## PERSONALITY AND SOUL
58 -
59 -Avoiding AI patterns is only half the job. Sterile, voiceless writing is just as obvious as slop.
60 -
61 -### Signs of soulless writing (even if technically "clean"):
62 -
63 -- Every sentence is the same length and structure
64 -- No opinions, just neutral reporting
65 -- No acknowledgment of uncertainty or mixed feelings
66 -- No first-person perspective when appropriate
67 -- No humor, no edge, no personality
68 -- Reads like a Wikipedia article or press release
69 -
70 -### How to add voice:
71 -
72 -**Have opinions.** Don't just report facts - react to them. "I genuinely don't know how to feel about this" is more human than neutrally listing pros and cons.
73 -
74 -**Vary your rhythm.** Short punchy sentences. Then longer ones that take their time getting where they're going. Mix it up.
75 -
76 -**Acknowledge complexity.** Real humans have mixed feelings. "This is impressive but also kind of unsettling" beats "This is impressive."
77 -
78 -**Use "I" when it fits.** First person isn't unprofessional - it's honest. "I keep coming back to..." or "Here's what gets me..." signals a real person thinking.
79 -
80 -**Let some mess in.** Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human.
81 -
82 -**Be specific about feelings.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am while nobody's watching."
83 -
84 -### Before (clean but soulless):
85 -
86 -> The experiment produced interesting results. The agents generated 3 million lines of code. Some developers were impressed while others were skeptical. The implications remain unclear.
87 -
88 -### After (has a pulse):
89 -
90 -> I genuinely don't know how to feel about this one. 3 million lines of code, generated while the humans presumably slept. Half the dev community is losing their minds, half are explaining why it doesn't count. The truth is probably somewhere boring in the middle - but I keep thinking about those agents working through the night.
91 -
92 ----
93 -
94 -## THE 24 PATTERNS
95 -
96 -### Content Patterns
97 -
98 -#### 1. Significance Inflation
99 -
100 -**Watch for:** stands/serves as, is a testament/reminder, a vital/significant/crucial/pivotal/key role/moment, underscores/highlights importance, reflects broader, symbolizing ongoing/enduring/lasting, marking/shaping the, represents a shift, key turning point, evolving landscape
101 -
102 -**Before:**
103 -> The Statistical Institute was officially established in 1989, marking a pivotal moment in the evolution of regional statistics.
104 -
105 -**After:**
106 -> The Statistical Institute was established in 1989 to collect and publish regional statistics.
107 -
108 -#### 2. Notability Name-Dropping
109 -
110 -**Watch for:** cited in NYT, BBC, FT; independent coverage; active social media presence; written by a leading expert
111 -
112 -**Before:**
113 -> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu.
114 -
115 -**After:**
116 -> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods.
117 -
118 -#### 3. Superficial -ing Analyses
119 -
120 -**Watch for:** highlighting/underscoring/emphasizing..., ensuring..., reflecting/symbolizing..., contributing to..., cultivating/fostering..., showcasing...
121 -
122 -**Before:**
123 -> The temple's colors resonate with natural beauty, symbolizing bluebonnets, reflecting the community's deep connection to the land.
124 -
125 -**After:**
126 -> The temple uses blue and gold colors. The architect said these were chosen to reference local bluebonnets.
127 -
128 -#### 4. Promotional Language
129 -
130 -**Watch for:** boasts a, vibrant, rich (figurative), profound, showcasing, exemplifies, commitment to, natural beauty, nestled, in the heart of, groundbreaking, renowned, breathtaking, must-visit, stunning
131 -
132 -**Before:**
133 -> Nestled within the breathtaking region, Alamata stands as a vibrant town with rich cultural heritage and stunning natural beauty.
134 -
135 -**After:**
136 -> Alamata is a town in the Gonder region, known for its weekly market and 18th-century church.
137 -
138 -#### 5. Vague Attributions
139 -
140 -**Watch for:** Industry reports, Observers have cited, Experts argue, Some critics argue, several sources/publications
141 -
142 -**Before:**
143 -> Experts believe it plays a crucial role in the regional ecosystem.
144 -
145 -**After:**
146 -> The river supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences.
147 -
148 -#### 6. Formulaic "Challenges" Sections
149 -
150 -**Watch for:** Despite its... faces several challenges..., Despite these challenges, Challenges and Legacy, Future Outlook
151 -
152 -**Before:**
153 -> Despite challenges typical of urban areas, the city continues to thrive as an integral part of growth.
154 -
155 -**After:**
156 -> Traffic congestion increased after 2015 when three new IT parks opened. The municipal corporation began a drainage project in 2022.
157 -
158 ----
159 -
160 -### Language Patterns
161 -
162 -#### 7. AI Vocabulary Words
163 -
164 -**High-frequency:** Additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (verb), interplay, intricate/intricacies, key (adjective), landscape (abstract), pivotal, showcase, tapestry (abstract), testament, underscore (verb), valuable, vibrant
165 -
166 -**Before:**
167 -> Additionally, a distinctive feature showcases how these dishes have integrated into the traditional culinary landscape.
168 -
169 -**After:**
170 -> Pasta dishes, introduced during Italian colonization, remain common, especially in the south.
171 -
172 -#### 8. Copula Avoidance
173 -
174 -**Watch for:** serves as/stands as/marks/represents [a], boasts/features/offers [a]
175 -
176 -**Before:**
177 -> Gallery 825 serves as the exhibition space. The gallery features four spaces and boasts over 3,000 square feet.
178 -
179 -**After:**
180 -> Gallery 825 is the exhibition space. The gallery has four rooms totaling 3,000 square feet.
181 -
182 -#### 9. Negative Parallelisms
183 -
184 -**Watch for:** "Not only...but...", "It's not just about..., it's..."
185 -
186 -**Before:**
187 -> It's not just about the beat; it's part of the aggression. It's not merely a song, it's a statement.
188 -
189 -**After:**
190 -> The heavy beat adds to the aggressive tone.
191 -
192 -#### 10. Rule of Three Overuse
193 -
194 -**Before:**
195 -> The event features keynote sessions, panel discussions, and networking opportunities. Attendees can expect innovation, inspiration, and industry insights.
196 -
197 -**After:**
198 -> The event includes talks and panels. There's also time for informal networking.
199 -
200 -#### 11. Synonym Cycling
201 -
202 -**Before:**
203 -> The protagonist faces challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home.
204 -
205 -**After:**
206 -> The protagonist faces many challenges but eventually triumphs and returns home.
207 -
208 -#### 12. False Ranges
209 -
210 -**Watch for:** "from X to Y" where X and Y aren't on a meaningful scale
211 -
212 -**Before:**
213 -> Our journey has taken us from the singularity of the Big Bang to the cosmic web, from the birth of stars to the dance of dark matter.
214 -
215 -**After:**
216 -> The book covers the Big Bang, star formation, and current theories about dark matter.
217 -
218 ----
219 -
220 -### Style Patterns
221 -
222 -#### 13. No Em Dashes
223 -
224 -Em dashes are the single biggest AI tell. Never use them. Replace with commas, parentheses, colons, or just split into two sentences.
225 -
226 -**Before:**
227 -> The term is promoted by institutions—not the people themselves—yet this continues—even in documents.
228 -
229 -**After:**
230 -> The term is promoted by institutions, not the people themselves, yet this continues in official documents.
231 -
232 -**Before:**
233 -> The tools are useful—especially for boilerplate—but they have limits.
234 -
235 -**After:**
236 -> The tools are useful (especially for boilerplate), but they have limits.
237 -
238 -**Before:**
239 -> There's one problem—nobody tested it.
240 -
241 -**After:**
242 -> There's one problem: nobody tested it.
243 -
244 -#### 14. Boldface Overuse
245 -
246 -**Before:**
247 -> It blends **OKRs**, **KPIs**, and tools such as the **Business Model Canvas** and **Balanced Scorecard**.
248 -
249 -**After:**
250 -> It blends OKRs, KPIs, and visual strategy tools like the Business Model Canvas and Balanced Scorecard.
251 -
252 -#### 15. Inline-Header Lists
253 -
254 -**Before:**
255 -> - **Performance:** Performance has been enhanced through optimized algorithms.
256 -> - **Security:** Security has been strengthened with encryption.
257 -
258 -**After:**
259 -> The update speeds up load times through optimized algorithms and adds end-to-end encryption.
260 -
261 -#### 16. Title Case Headings
262 -
263 -**Before:**
264 -> ## Strategic Negotiations And Global Partnerships
265 -
266 -**After:**
267 -> ## Strategic negotiations and global partnerships
268 -
269 -#### 17. Emojis in Professional Writing
270 -
271 -**Before:**
272 -> 🚀 **Launch Phase:** The product launches in Q3
273 -> 💡 **Key Insight:** Users prefer simplicity
274 -
275 -**After:**
276 -> The product launches in Q3. User research showed a preference for simplicity.
277 -
278 -#### 18. Curly Quotation Marks
279 -
280 -**Before:**
281 -> He said "the project is on track" but others disagreed.
282 -
283 -**After:**
284 -> He said "the project is on track" but others disagreed.
285 -
286 ----
287 -
288 -### Communication Patterns
289 -
290 -#### 19. Chatbot Artifacts
291 -
292 -**Watch for:** I hope this helps, Of course!, Certainly!, You're absolutely right!, Would you like..., let me know, here is a...
293 -
294 -**Before:**
295 -> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand on any section.
296 -
297 -**After:**
298 -> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest.
299 -
300 -#### 20. Knowledge-Cutoff Disclaimers
301 -
302 -**Watch for:** as of [date], Up to my last training update, While specific details are limited/scarce..., based on available information...
303 -
304 -**Before:**
305 -> While specific details about the company's founding are not extensively documented in readily available sources, it appears to have been established sometime in the 1990s.
306 -
307 -**After:**
308 -> The company was founded in 1994, according to its registration documents.
309 -
310 -#### 21. Sycophantic Tone
311 -
312 -**Before:**
313 -> Great question! You're absolutely right that this is a complex topic. That's an excellent point!
314 -
315 -**After:**
316 -> The economic factors you mentioned are relevant here.
317 -
318 ----
319 -
320 -### Filler and Hedging
321 -
322 -#### 22. Filler Phrases
323 -
324 -| Before | After |
325 -|--------|-------|
326 -| "In order to achieve this" | "To achieve this" |
327 -| "Due to the fact that" | "Because" |
328 -| "At this point in time" | "Now" |
329 -| "It is important to note that" | (delete) |
330 -| "has the ability to" | "can" |
331 -
332 -#### 23. Excessive Hedging
333 -
334 -**Before:**
335 -> It could potentially possibly be argued that the policy might have some effect on outcomes.
336 -
337 -**After:**
338 -> The policy may affect outcomes.
339 -
340 -#### 24. Generic Positive Conclusions
341 -
342 -**Before:**
343 -> The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence.
344 -
345 -**After:**
346 -> The company plans to open two more locations next year.
347 -
348 ----
349 -
350 -## Process
351 -
352 -1. Read the input text carefully
353 -2. Identify all instances of the 24 patterns
354 -3. Rewrite problematic sections
355 -4. Ensure the revised text:
356 - - Sounds natural when read aloud
357 - - Varies sentence structure
358 - - Uses specific details over vague claims
359 - - Uses simple constructions (is/are/has) where appropriate
360 -5. **Draft humanized version**
361 -6. **Ask:** "What makes this still obviously AI generated?"
362 -7. **List** remaining tells (if any)
363 -8. **Revise** based on the audit
364 -9. **Present final version**
365 -
366 -## Output Format
367 -
368 -```
369 -## Draft Rewrite
370 -
371 -[First pass at humanized text]
372 -
373 -## AI Audit
374 -
375 -What still reads as AI-generated:
376 -- [bullet points of remaining tells]
377 -
378 -## Final Version
379 -
380 -[Revised text after audit]
381 -
382 -## Changes Made
383 -
384 -- [Brief summary of transformations applied]
385 -```
386 -
387 ----
388 -
389 -## Full Example
390 -
391 -**Input (AI-heavy):**
392 -
393 -> Great question! Here is an essay on this topic. I hope this helps!
394 ->
395 -> AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools—nestled at the intersection of research and practice—are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows.
396 -
397 -**Draft Rewrite:**
398 -
399 -> AI coding assistants speed up some tasks. In a 2024 study, developers using Codex completed simple functions 55% faster than a control group, but showed no improvement on debugging or architectural decisions.
400 ->
401 -> The tools are good at boilerplate: config files, test scaffolding, repetitive refactors. They are bad at knowing when they are wrong.
402 -
403 -**AI Audit:**
404 -
405 -What still reads as AI-generated:
406 -- Rhythm is still a bit too tidy (clean contrasts, evenly paced)
407 -- The closer leans slightly slogan-y
408 -
409 -**Final Version:**
410 -
411 -> AI coding assistants can make you faster at the boring parts. Not everything. Definitely not architecture.
412 ->
413 -> They're great at boilerplate: config files, test scaffolding, repetitive refactors. They're also great at sounding right while being wrong. I've accepted suggestions that compiled, passed lint, and still missed the point because I stopped paying attention.
414 ->
415 -> If you don't have tests, you're basically guessing.
416 -
417 -**Changes Made:**
418 -
419 -- Removed chatbot artifacts ("Great question!", "I hope this helps!")
420 -- Removed significance inflation ("testament", "pivotal moment", "evolving landscape")
421 -- Removed promotional language ("groundbreaking", "nestled")
422 -- Removed em dashes
423 -- Removed copula avoidance ("serves as") → used direct statements
424 -- Added first-person voice and opinion
425 -- Varied sentence rhythm
426 -
427 ----
428 -
429 -## Reference
430 -
431 -Based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), maintained by WikiProject AI Cleanup.
.agents/skills/sdk-dart/SKILL.md deleted
-1004
@@ -1,1004 +0,0 @@
1 ----
2 -name: sdk-dart
3 -description: Dart/Flutter SDK skill for the onde_inference pub.dev package. Covers flutter_rust_bridge v2 codegen, CocoaPods podspec patterns, ExternalLibrary.open init, .pubignore, Xcode 26 compatibility, Android NDK config, and pub.dev publishing. Apply to all Dart SDK edits.
4 -allowed-tools: Read, Write, Edit, Glob, Grep
5 -user-invocable: false
6 ----
7 -
8 -# SKILL: Dart / Flutter SDK with flutter_rust_bridge v2
9 -
10 -> Captured from: building `onde_inference` pub.dev package wrapping the `onde` Rust crate.
11 -> Agent reference for future Dart SDK tasks in this repo.
12 -
13 ----
14 -
15 -## What This Skill Covers
16 -
17 -End-to-end creation of a pub.dev-ready Flutter FFI plugin package that bridges to a
18 -Rust library using `flutter_rust_bridge` v2. Covers the full lifecycle:
19 -design → Rust bridge crate → Dart library → platform build files → tests →
20 -pub.dev dry-run → codegen → `flutter run`.
21 -
22 ----
23 -
24 -## Hard-Won Lessons (Read First)
25 -
26 -These are non-obvious facts that caused real failures and are not in the FRB docs.
27 -
28 -### 1. `flutter_rust_bridge_codegen` is a Rust binary — NOT a Dart package
29 -
30 -```
31 -# Correct — install the Rust binary once:
32 -cargo install flutter_rust_bridge_codegen
33 -
34 -# WRONG — this package does not exist on pub.dev:
35 -# dev_dependencies:
36 -# flutter_rust_bridge_codegen: ^2.12.0 ← will fail `dart pub get`
37 -```
38 -
39 -Never add it to `pubspec.yaml` `dev_dependencies`.
40 -
41 -### 2. `flutter_rust_bridge.yaml` — use module path syntax, not file paths
42 -
43 -```yaml
44 -# FRB 2.x CORRECT syntax:
45 -rust_input: "crate::api"
46 -rust_root: "rust"
47 -
48 -# WRONG (causes parse error in FRB 2.x):
49 -# rust_input:
50 -# - "rust/src/api.rs"
51 -
52 -# WRONG (causes migration error in FRB 2.x):
53 -# rust_input: "rust/src/api.rs"
54 -```
55 -
56 -### 3. Delete `frb_generated.rs` before running codegen — never pre-create it
57 -
58 -FRB codegen uses an **exclusive create** for `rust/src/frb_generated.rs`.
59 -If the file exists (even as a stub), codegen aborts with `File exists (os error 17)`.
60 -
61 -```bash
62 -# Before first codegen run:
63 -rm -f rust/src/frb_generated.rs
64 -
65 -# frb_generated.dart (the Dart stub) CAN pre-exist — FRB overwrites it safely.
66 -```
67 -
68 -### 4. `cargo-expand` is required by FRB codegen — auto-installs but takes time
69 -
70 -FRB codegen runs `cargo expand` internally to parse macro-expanded Rust.
71 -If missing, it installs automatically but adds ~2 min to the first run.
72 -Pre-install it:
73 -
74 -```bash
75 -cargo install cargo-expand
76 -```
77 -
78 -### 5. Example apps need `flutter create .` — a `pubspec.yaml` alone is not enough
79 -
80 -A Flutter project requires platform scaffolding (`ios/`, `android/`, `macos/`,
81 -`Runner.xcodeproj`, etc.) that only `flutter create` generates.
82 -
83 -```bash
84 -cd sdk/dart/example
85 -flutter create . \
86 - --project-name onde_inference_example \
87 - --platforms=ios,macos,android,linux,windows \
88 - --org com.ondeinference
89 -```
90 -
91 -Run this **before** `flutter pub get` or `flutter run`.
92 -
93 -### 6. Types with `#[uniffi]` cannot be re-used directly in FRB
94 -
95 -If the source Rust crate annotates types with `#[uniffi::Record]`, `#[uniffi::Enum]`,
96 -etc., those attributes conflict with FRB code generation. The fix is **type mirroring**:
97 -re-declare every bridged type as a plain struct/enum in the bridge crate (`rust/src/api.rs`)
98 -and implement bidirectional `From<>` conversions.
99 -
100 -```rust
101 -// In rust/src/api.rs — mirror type (no #[uniffi] here)
102 -#[derive(Debug, Clone)]
103 -pub struct ChatMessage { pub role: ChatRole, pub content: String }
104 -
105 -// Conversion to/from the onde crate's type
106 -impl From<OndeChatMessage> for ChatMessage { ... }
107 -impl From<ChatMessage> for OndeChatMessage { ... }
108 -```
109 -
110 -### 7. `StreamSink<T>` requires `T: SseEncode` — satisfied by codegen, not by hand
111 -
112 -The `StreamSink` type (used for streaming) demands `T: SseEncode`. The real impl
113 -is generated by codegen. The pre-codegen stub in `frb_generated.rs` must provide
114 -a no-op impl to let `cargo check` pass:
115 -
116 -```rust
117 -// rust/src/frb_generated.rs (pre-codegen stub)
118 -use flutter_rust_bridge::for_generated::SseCodec;
119 -flutter_rust_bridge::frb_generated_boilerplate!(
120 - default_stream_sink_codec = SseCodec,
121 - default_rust_opaque = RustOpaqueMoi,
122 - default_rust_auto_opaque = RustAutoOpaqueMoi,
123 -);
124 -impl SseEncode for crate::api::StreamChunk {
125 - fn sse_encode(self, _s: &mut flutter_rust_bridge::for_generated::SseSerializer) {
126 - unimplemented!("codegen stub")
127 - }
128 -}
129 -```
130 -
131 -### 8. Dart lint: no leading underscore for library import prefixes
132 -
133 -```dart
134 -// BAD — triggers `no_leading_underscores_for_library_prefixes` lint:
135 -import 'frb_generated.dart' as _frb;
136 -
137 -// GOOD:
138 -import 'frb_generated.dart' as frb;
139 -```
140 -
141 -### 9. Dart lint: remove `library <name>;` directive
142 -
143 -```dart
144 -// BAD — triggers `unnecessary_library_name` lint:
145 -library onde_inference;
146 -
147 -// GOOD:
148 -library;
149 -```
150 -
151 -### 10. Exclude FRB-generated files from Dart analysis
152 -
153 -```yaml
154 -# analysis_options.yaml
155 -analyzer:
156 - exclude:
157 - - lib/src/frb_generated.dart
158 - - lib/src/frb_generated.io.dart
159 - - lib/src/frb_generated.web.dart
160 -```
161 -
162 ----
163 -
164 -### 11. Flutter version is a single source of truth — never hardcode it in CI
165 -
166 -The CI workflow reads the Flutter version from `sdk/dart/.flutter-version` via
167 -`subosito/flutter-action`'s `flutter-version-file:` parameter. **Never
168 -hardcode `flutter-version: "x.y.z"` directly in the workflow YAML.**
169 -
170 -```
171 -sdk/dart/.flutter-version ← only place the Flutter version is written
172 -```
173 -
174 -```yaml
175 -# .github/workflows/release-sdk-dart.yml (both jobs)
176 -- name: Set up Flutter
177 - uses: subosito/flutter-action@v2
178 - with:
179 - flutter-version-file: sdk/dart/.flutter-version # reads .flutter-version
180 - channel: stable
181 - cache: true
182 -```
183 -
184 -**Why this matters:** Each Flutter release ships a specific Dart SDK version.
185 -Dev dependencies like `freezed` have minimum Dart SDK requirements that advance
186 -faster than Flutter stable releases. When you bump a Flutter-sensitive dep,
187 -you must also bump the Flutter version used in CI — and `.flutter-version` is
188 -the one place to do that.
189 -
190 -**Rule: when bumping a dep that constrains the Dart SDK** (e.g. `freezed`,
191 -`flutter_rust_bridge`, `build_runner`):
192 -
193 -1. Run `flutter pub get` locally — a version-solving failure tells you
194 - immediately if the Dart SDK shipped with the current Flutter is too old.
195 -2. Find the Flutter release that ships the required Dart SDK version
196 - (see https://docs.flutter.dev/release/archive).
197 -3. Update `sdk/dart/.flutter-version` to that version.
198 -4. Tighten `pubspec.yaml`'s `environment.sdk` lower bound to match — it must
199 - honestly reflect what your deps need, not what you originally wrote.
200 -
201 -**`pubspec.yaml` environment constraint must be accurate:**
202 -
203 -```yaml
204 -# WRONG — lying lower bound causes CI to pass locally but fail on old runners
205 -environment:
206 - sdk: ">=3.3.0 <4.0.0" # freezed ^3.2.x actually requires >=3.8.0
207 -
208 -# CORRECT
209 -environment:
210 - sdk: ">=3.8.0 <4.0.0"
211 - flutter: ">=3.32.0"
212 -```
213 -
214 -An inaccurate `sdk:` lower bound is invisible locally (you're already on a
215 -newer SDK) but breaks any runner or user on the stated minimum.
216 -
217 ----
218 -
219 -## Package Structure Reference
220 -
221 -```
222 -sdk/dart/
223 -├── pubspec.yaml # Flutter plugin, ffiPlugin:true per platform
224 -├── analysis_options.yaml # Exclude frb_generated files
225 -├── flutter_rust_bridge.yaml # Codegen config (rust_input: "crate::api")
226 -├── CHANGELOG.md
227 -├── LICENSE
228 -├── README.md # pub.dev landing page
229 -│
230 -├── rust/ # Rust bridge crate
231 -│ ├── Cargo.toml # crate-type=[cdylib,staticlib]; publish=false
232 -│ └── src/
233 -│ ├── lib.rs # mod frb_generated; pub mod api; + #[frb(init)]
234 -│ ├── api.rs # Mirror types + OndeChatEngine + free fns
235 -│ └── frb_generated.rs # DELETE before codegen; stub only pre-codegen
236 -│
237 -├── lib/
238 -│ ├── <package_name>.dart # Public barrel — exports types + engine + RustLib
239 -│ └── src/
240 -│ ├── types.dart # Standalone Dart types — NO native dependency
241 -│ ├── engine.dart # OndeChatEngine wrapper + static helpers
242 -│ ├── frb_generated.dart # CAN be stub pre-codegen; overwritten by codegen
243 -│ └── frb_generated_stub.dart # Hand-written stub: RustLib + OndeChatEngine
244 -│
245 -├── android/
246 -│ ├── build.gradle # ffiPlugin CMake integration
247 -│ └── CMakeLists.txt # cargo build per Android ABI
248 -├── ios/
249 -│ └── <package>.podspec # script_phase: cargo build aarch64-apple-ios[-sim]
250 -├── macos/
251 -│ └── <package>.podspec # script_phase: cargo build aarch64-apple-darwin
252 -├── linux/
253 -│ ├── CMakeLists.txt
254 -│ └── <package>_plugin.cc
255 -├── windows/
256 -│ ├── CMakeLists.txt
257 -│ └── <package>_plugin.cpp
258 -│
259 -├── test/
260 -│ └── dart_test.dart # 22+ unit tests — all pass WITHOUT native binary
261 -└── example/
262 - ├── pubspec.yaml
263 - └── lib/main.dart # Full Material 3 demo app
264 -```
265 -
266 ----
267 -
268 -## `pubspec.yaml` Template
269 -
270 -```yaml
271 -name: <package_name> # e.g. onde_inference (lowercase_snake)
272 -description: >-
273 - One-liner for pub.dev search (80 chars max for score).
274 -version: 0.1.0
275 -repository: https://github.com/ondeinference/onde
276 -homepage: https://ondeinference.com
277 -issue_tracker: https://github.com/ondeinference/onde/issues
278 -
279 -environment:
280 - sdk: '>=3.8.0 <4.0.0' # must reflect the actual minimum required by deps
281 - flutter: '>=3.32.0' # Flutter 3.32 ships Dart 3.8; update .flutter-version too
282 -
283 -dependencies:
284 - flutter:
285 - sdk: flutter
286 - flutter_rust_bridge: ^2.12.0 # Must match Rust crate version exactly
287 -
288 -dev_dependencies:
289 - flutter_test:
290 - sdk: flutter
291 - test: ^1.25.0 # For pure-Dart unit tests
292 - flutter_lints: ^5.0.0
293 - # NOTE: flutter_rust_bridge_codegen is a Rust binary, NOT a Dart package.
294 - # Install: cargo install flutter_rust_bridge_codegen
295 -
296 -flutter:
297 - plugin:
298 - platforms:
299 - android:
300 - ffiPlugin: true
301 - ios:
302 - ffiPlugin: true
303 - macos:
304 - ffiPlugin: true
305 - linux:
306 - ffiPlugin: true
307 - windows:
308 - ffiPlugin: true
309 -```
310 -
311 ----
312 -
313 -## `flutter_rust_bridge.yaml` Template
314 -
315 -```yaml
316 -rust_root: "rust"
317 -rust_input: "crate::api" # Module path, not file path
318 -dart_output: "lib/src/frb_generated.dart"
319 -dart_entrypoint_class_name: RustLib
320 -dart_preamble: |
321 - // ignore_for_file: type=lint, unused_import, unnecessary_import
322 -web: false # On-device inference requires native access
323 -```
324 -
325 ----
326 -
327 -## Rust Bridge Crate (`rust/Cargo.toml`) Template
328 -
329 -```toml
330 -[package]
331 -name = "<package>_dart" # e.g. onde_inference_dart
332 -version = "0.1.0"
333 -edition = "2021"
334 -publish = false # Bridge crate is not published to crates.io
335 -
336 -[lib]
337 -crate-type = ["cdylib", "staticlib"]
338 -# cdylib → .so / .dylib / .dll for Android / macOS / Linux / Windows
339 -# staticlib → .a for iOS (dynamic linking not allowed)
340 -
341 -[dependencies]
342 -flutter_rust_bridge = "=2.12.0" # Must match Dart package version exactly
343 -<source_crate> = { path = "../../.." } # Relative path to the wrapped crate
344 -tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
345 -thiserror = "2"
346 -log = "0.4"
347 -```
348 -
349 ----
350 -
351 -## Rust `src/lib.rs` Template
352 -
353 -```rust
354 -mod frb_generated; // Generated by flutter_rust_bridge_codegen
355 -pub mod api;
356 -
357 -/// Called once when the Dart RustLib.init() is invoked.
358 -#[flutter_rust_bridge::frb(init)]
359 -pub fn init_app() {
360 - flutter_rust_bridge::setup_default_user_utils();
361 -}
362 -```
363 -
364 ----
365 -
366 -## Type Mirroring Pattern in `src/api.rs`
367 -
368 -Use this pattern for every Rust type that crosses the bridge.
369 -
370 -```rust
371 -use flutter_rust_bridge::frb;
372 -use <source_crate>::inference::{
373 - ChatMessage as SrcChatMessage,
374 - ChatRole as SrcChatRole,
375 - // ...
376 -};
377 -
378 -// 1. Declare a mirror type with no framework annotations
379 -#[derive(Debug, Clone, PartialEq)]
380 -pub enum ChatRole { System, User, Assistant }
381 -
382 -// 2. Bidirectional conversions
383 -impl From<SrcChatRole> for ChatRole { ... }
384 -impl From<ChatRole> for SrcChatRole { ... }
385 -
386 -// 3. Opaque engine wrapper — FRB auto-wraps in Arc<RwLock<T>>
387 -pub struct OndeChatEngine { inner: <source_crate>::inference::ChatEngine }
388 -
389 -impl OndeChatEngine {
390 - #[frb(sync)] // sync constructor → no Future on Dart side
391 - pub fn new() -> OndeChatEngine { OndeChatEngine { inner: ChatEngine::new() } }
392 -
393 - pub async fn send_message(&self, message: String) -> Result<InferenceResult, OndeError> {
394 - self.inner.send_message(message).await
395 - .map(InferenceResult::from)
396 - .map_err(OndeError::from)
397 - }
398 -
399 - // Streaming: use StreamSink (maps to Dart Stream<T>)
400 - pub async fn stream_message(
401 - &self,
402 - message: String,
403 - sink: StreamSink<StreamChunk>,
404 - ) -> Result<(), OndeError> {
405 - let mut rx = self.inner.stream_message(message).await.map_err(OndeError::from)?;
406 - while let Some(chunk) = rx.recv().await {
407 - let done = chunk.done;
408 - sink.add(StreamChunk::from(chunk));
409 - if done { break; }
410 - }
411 - Ok(())
412 - }
413 -}
414 -
415 -// Free functions with #[frb(sync)] → synchronous on Dart side
416 -#[frb(sync)]
417 -pub fn default_model_config() -> GgufModelConfig {
418 - <source_crate>::inference::GgufModelConfig::platform_default().into()
419 -}
420 -```
421 -
422 ----
423 -
424 -## Dart Stub Pattern (`lib/src/frb_generated_stub.dart`)
425 -
426 -The stub lets the package compile and all pure-Dart unit tests pass **before**
427 -the native binary exists. Key rules:
428 -
429 -1. Import only `types.dart` — never `dart:ffi` or anything platform-native.
430 -2. `RustLib.init()` sets a bool flag; `new_()` and every inference method throw
431 - `UnimplementedError`.
432 -3. Config/sampling free functions return **real hardcoded values** (matching the
433 - Rust constants) so model-selection UIs can be built and tested immediately.
434 -4. Make every method signature match exactly what FRB will generate (same names,
435 - same parameter types, same return types) — this makes the post-codegen
436 - transition a drop-in replacement.
437 -
438 -```dart
439 -// frb_generated.dart (stub redirect)
440 -export 'frb_generated_stub.dart';
441 -
442 -// frb_generated_stub.dart
443 -import 'types.dart';
444 -
445 -class RustLib {
446 - static bool _initialized = false;
447 - static bool get isInitialized => _initialized;
448 - static Future<void> init() async { _initialized = true; }
449 -}
450 -
451 -class OndeChatEngine {
452 - static Future<OndeChatEngine> new_() async =>
453 - throw UnimplementedError('Run flutter_rust_bridge_codegen generate');
454 -
455 - Future<double> loadDefaultModel({ String? systemPrompt, SamplingConfig? sampling }) =>
456 - throw UnimplementedError(_msg('loadDefaultModel'));
457 -
458 - Stream<StreamChunk> streamMessage({ required String message }) =>
459 - Stream.error(UnimplementedError(_msg('streamMessage')), StackTrace.current);
460 -
461 - static String _msg(String m) => 'OndeChatEngine.$m: run codegen first';
462 -}
463 -
464 -// Free functions — return real data so UI can be built/tested without native
465 -GgufModelConfig qwen251_5bConfig() => const GgufModelConfig(
466 - modelId: 'bartowski/Qwen2.5-1.5B-Instruct-GGUF',
467 - files: ['Qwen2.5-1.5B-Instruct-Q4_K_M.gguf'],
468 - displayName: 'Qwen 2.5 1.5B',
469 - approxMemory: '~941 MB',
470 -);
471 -```
472 -
473 ----
474 -
475 -## Dart Public API Pattern (`lib/src/engine.dart`)
476 -
477 -```dart
478 -import 'dart:async';
479 -import 'dart:io' show Platform;
480 -
481 -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
482 - show ExternalLibrary;
483 -
484 -import 'frb_generated.dart/frb_generated.dart' as frb;
485 -import 'frb_generated.dart/api.dart' as api;
486 -import 'types.dart';
487 -
488 -export 'frb_generated.dart/frb_generated.dart' show RustLib;
489 -export 'frb_generated.dart/api.dart'
490 - show OndeChatEngine, OndeError, OndeError_NoModelLoaded, /* ... */;
491 -
492 -/// Extension on the FRB-generated OndeChatEngine opaque type.
493 -extension OndeChatEngineX on api.OndeChatEngine {
494 - Future<double> loadDefaultModel({
495 - String? systemPrompt,
496 - SamplingConfig? sampling,
497 - }) =>
498 - loadGgufModel(
499 - config: api.defaultModelConfig(),
500 - systemPrompt: systemPrompt,
501 - sampling: sampling,
502 - );
503 -
504 - Future<int> clearHistoryCount() async => (await clearHistory()).toInt();
505 -}
506 -
507 -abstract final class OndeInference {
508 - /// Initialises the Rust shared library.
509 - ///
510 - /// On macOS and iOS the Rust static library is force-loaded into the
511 - /// onde_inference CocoaPods framework via OTHER_LDFLAGS in the podspec.
512 - /// We must open that framework explicitly — DynamicLibrary.process()
513 - /// (RTLD_DEFAULT) cannot resolve symbols inside a separately-loaded
514 - /// framework.
515 - static Future<void> init() async {
516 - ExternalLibrary? lib;
517 - if (Platform.isMacOS || Platform.isIOS) {
518 - lib = ExternalLibrary.open(
519 - 'onde_inference.framework/onde_inference',
520 - );
521 - }
522 - await frb.RustLib.init(externalLibrary: lib);
523 - }
524 -
525 - static GgufModelConfig defaultModelConfig() => api.defaultModelConfig();
526 - static SamplingConfig deterministicSamplingConfig() => api.deterministicSamplingConfig();
527 -}
528 -```
529 -
530 -### Why `ExternalLibrary.open(...)` and not `.process()`
531 -
532 -The `-force_load` linker flag embeds Rust symbols into the **pod framework**
533 -(`onde_inference.framework/onde_inference`), NOT into the main executable.
534 -`DynamicLibrary.process()` maps to `RTLD_DEFAULT`, which only searches the
535 -main executable and already-opened images in certain configurations. On
536 -macOS/iOS with CocoaPods `use_frameworks!`, the pod framework is a separate
537 -Mach-O image — you must `dlopen` it by name.
538 -
539 -FRB 2.12's `ExternalLibrary.open(path)` calls `DynamicLibrary.open(path)`
540 -under the hood. On non-Apple platforms (Android, Linux, Windows) the default
541 -`ExternalLibraryLoaderConfig` stem-based lookup works, so `lib` stays `null`
542 -and `RustLib.init()` handles it automatically.
543 -
544 -**Do NOT use `ExternalLibrary.process(iKnowHowToUseIt: true)`** — it will
545 -produce `Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...)`.
546 -
547 ----
548 -
549 -## Platform Build Files
550 -
551 -### Dummy Swift Plugin Classes (required)
552 -
553 -Both `macos/Classes/OndeInferencePlugin.swift` and `ios/Classes/OndeInferencePlugin.swift`
554 -**must exist** — they are no-op `FlutterPlugin` stubs. Without them, CocoaPods
555 -has no source files to compile and won't produce a real `.framework` target.
556 -The Rust static library is force-loaded INTO this framework.
557 -
558 -```swift
559 -// macos/Classes/OndeInferencePlugin.swift
560 -import FlutterMacOS
561 -
562 -public class OndeInferencePlugin: NSObject, FlutterPlugin {
563 - public static func register(with registrar: FlutterPluginRegistrar) {
564 - // No-op — all FFI symbols come from the Rust static library.
565 - }
566 -}
567 -```
568 -
569 -```swift
570 -// ios/Classes/OndeInferencePlugin.swift
571 -import Flutter // NOT FlutterMacOS
572 -
573 -public class OndeInferencePlugin: NSObject, FlutterPlugin {
574 - public static func register(with registrar: FlutterPluginRegistrar) {}
575 -}
576 -```
577 -
578 -### macOS podspec — full working version
579 -
580 -```ruby
581 -Pod::Spec.new do |s|
582 - s.name = 'onde_inference'
583 - s.version = '0.1.0'
584 - s.summary = 'On-device LLM inference SDK for Flutter (macOS).'
585 - s.description = 'Runs Qwen 2.5 models locally on macOS with Metal acceleration.'
586 - s.homepage = 'https://ondeinference.com'
587 - s.license = { :type => 'MIT', :file => '../LICENSE' }
588 - s.author = { 'Onde Inference' => 'hello@ondeinference.com' }
589 - s.source = { :path => '.' }
590 - s.source_files = 'Classes/**/*'
591 - s.dependency 'FlutterMacOS'
592 - s.platform = :osx, '10.15'
593 - s.swift_version = '5.0'
594 -
595 - s.script_phases = [
596 - {
597 - :name => 'Build Rust bridge (onde_inference_dart)',
598 - :script => <<~SHELL,
599 - set -e
600 - RUST_DIR="${PODS_TARGET_SRCROOT}/../rust"
601 - cd "$RUST_DIR"
602 - ARCH=$(uname -m)
603 - if [[ "$ARCH" == "arm64" ]]; then
604 - TARGET="aarch64-apple-darwin"
605 - else
606 - TARGET="x86_64-apple-darwin"
607 - fi
608 - cargo build --release --target "$TARGET"
609 - cp "target/$TARGET/release/libonde_inference_dart.a" \
610 - "${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"
611 - SHELL
612 - :execution_position => :before_compile,
613 - :output_files => ["${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"],
614 - }
615 - ]
616 -
617 - # SystemConfiguration is required by Rust hyper_util (proxy detection)
618 - s.frameworks = 'SystemConfiguration'
619 -
620 - s.pod_target_xcconfig = {
621 - 'DEFINES_MODULE' => 'YES',
622 - 'OTHER_LDFLAGS' => '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
623 - }
624 - s.preserve_paths = '../rust/**/*'
625 -end
626 -```
627 -
628 -**Key points:**
629 -- `s.frameworks = 'SystemConfiguration'` — required because the Rust `hyper_util`
630 - crate (transitive via `reqwest`) references `SCDynamicStoreCopyProxies`,
631 - `SCNetworkReachabilityCreateWithName`, etc. Without it you get
632 - `ld: symbol(s) not found for architecture arm64`.
633 -- `-force_load` links ALL symbols from the `.a` into the pod framework.
634 -- `s.source_files = 'Classes/**/*'` picks up the dummy Swift plugin.
635 -
636 -### iOS podspec — differs from macOS
637 -
638 -```ruby
639 - s.dependency 'Flutter' # NOT 'FlutterMacOS'
640 - s.platform = :ios, '13.0' # NOT :osx
641 -
642 - # Target selection differs:
643 - # iphonesimulator → aarch64-apple-ios-sim
644 - # iphoneos → aarch64-apple-ios
645 -
646 - s.pod_target_xcconfig = {
647 - 'DEFINES_MODULE' => 'YES',
648 - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
649 - 'OTHER_LDFLAGS' =>
650 - '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
651 - }
652 -```
653 -
654 -### Android `CMakeLists.txt` — key section
655 -
656 -```cmake
657 -if(ANDROID_ABI STREQUAL "arm64-v8a")
658 - set(CARGO_TARGET "aarch64-linux-android")
659 -elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
660 - set(CARGO_TARGET "armv7-linux-androideabi")
661 -elseif(ANDROID_ABI STREQUAL "x86_64")
662 - set(CARGO_TARGET "x86_64-linux-android")
663 -elseif(ANDROID_ABI STREQUAL "x86")
664 - set(CARGO_TARGET "i686-linux-android")
665 -endif()
666 -add_custom_command(OUTPUT "${LIB_OUTPUT}"
667 - COMMAND cargo build --release --target ${CARGO_TARGET}
668 - --manifest-path "${RUST_DIR}/Cargo.toml"
669 - WORKING_DIRECTORY ${RUST_DIR})
670 -add_library(onde_inference_dart SHARED IMPORTED GLOBAL)
671 -set_target_properties(onde_inference_dart PROPERTIES IMPORTED_LOCATION ${LIB_OUTPUT})
672 -```
673 -
674 -Android uses CMake to invoke `cargo build` for the right NDK triple, producing
675 -a `.so` shared library (unlike Apple which uses `.a` staticlibs).
676 -
677 -### Android NDK Linker Config (`rust/.cargo/config.toml`)
678 -
679 -```toml
680 -[target.aarch64-linux-android]
681 -linker = "aarch64-linux-android24-clang"
682 -
683 -[target.armv7-linux-androideabi]
684 -linker = "armv7a-linux-androideabi24-clang"
685 -
686 -[target.x86_64-linux-android]
687 -linker = "x86_64-linux-android24-clang"
688 -
689 -[target.i686-linux-android]
690 -linker = "i686-linux-android24-clang"
691 -```
692 -
693 -These short names require the NDK toolchain `bin/` to be on PATH.
694 -Flutter's Gradle + CMake integration typically handles this automatically
695 -during `flutter build apk` / `flutter run -d <android>`.
696 -
697 ----
698 -
699 -## Testing Strategy
700 -
701 -All pure-Dart unit tests run **without** building the native binary.
702 -
703 -```
704 -Tests that PASS pre-codegen (using the stub):
705 - ✓ All type constructors, equality, hashCode, copyWith
706 - ✓ Enum toString and isReady helpers
707 - ✓ SamplingConfig presets (default, deterministic, mobile)
708 - ✓ GgufModelConfig factory functions (real hardcoded values in stub)
709 - ✓ RustLib.init() is idempotent
710 - ✓ OndeInference.* static helpers
711 - ✓ OndeChatEngine.create() throws UnimplementedError pre-codegen
712 -
713 -Tests that require codegen + native binary:
714 - ✗ Any actual inference call
715 - ✗ loadDefaultModel / sendMessage / streamMessage
716 -```
717 -
718 -Run tests:
719 -```bash
720 -cd sdk/dart && dart test test/dart_test.dart
721 -```
722 -
723 ----
724 -
725 -## Pub.dev Readiness Checklist
726 -
727 -Run this before declaring the SDK ready:
728 -
729 -```bash
730 -# 1. Dart analysis — must be zero issues
731 -dart analyze lib/
732 -
733 -# 2. All tests pass
734 -dart test test/dart_test.dart
735 -
736 -# 3. Dry-run publish — must show compressed size < 100 MB and minimal warnings
737 -dart pub publish --dry-run
738 -```
739 -
740 -Common pub.dev score deductions to avoid:
741 -- Missing `repository:` in pubspec → loses points
742 -- No `example/` directory → loses points
743 -- `description:` under 60 chars or over 180 chars → loses points
744 -- Library exports not documented → loses points
745 -- `library <name>;` directive → triggers lint
746 -
747 -### Pub.dev Upload Size (100 MB limit)
748 -
749 -`dart pub publish` bundles everything not excluded by `.pubignore` (or
750 -`.gitignore` if `.pubignore` doesn't exist). The Rust `target/` dir alone
751 -can be **7+ GB**. A `.pubignore` file is mandatory.
752 -
753 -**Files that MUST be included** (consumers build from source):
754 -- `rust/src/`, `rust/Cargo.toml`, `rust/Cargo.lock`
755 -- `ios/`, `macos/`, `android/`, `linux/`, `windows/` (build scripts, podspecs, CMakeLists)
756 -- `ios/Classes/`, `macos/Classes/` (dummy plugin Swift files)
757 -- `lib/` (all Dart code)
758 -
759 -**Files that MUST be excluded** (`.pubignore`):
760 -- `rust/target/` — multi-GB build artifacts
761 -- `**/*.a`, `**/*.so`, `**/*.dylib`, `**/*.dll` — precompiled binaries
762 -- `.dart_tool/`, `pubspec.lock`
763 -- `example/build/`, `example/.dart_tool/`
764 -- `logs/`, `.idea/`, `.vscode/`, `.DS_Store`
765 -
766 -After adding `.pubignore`, verify: `dart pub publish --dry-run` should report
767 -~280 KB compressed size, not hundreds of MB.
768 -
769 ----
770 -
771 -## Codegen Workflow (Full, Correct Order)
772 -
773 -```bash
774 -# ── Prerequisites (one-time) ──────────────────────────────────────────────
775 -
776 -rustup target add aarch64-apple-ios aarch64-apple-ios-sim # iOS
777 -rustup target add aarch64-apple-darwin x86_64-apple-darwin # macOS
778 -
779 -cargo install cargo-expand # Required by FRB codegen parser
780 -cargo install flutter_rust_bridge_codegen # The codegen tool itself
781 -
782 -# ── Per-package setup ─────────────────────────────────────────────────────
783 -
784 -cd sdk/dart
785 -
786 -# IMPORTANT: delete the pre-codegen Rust stub first
787 -rm -f rust/src/frb_generated.rs
788 -
789 -# Run codegen — generates:
790 -# rust/src/frb_generated.rs (Rust glue)
791 -# lib/src/frb_generated.dart (Dart FFI bindings, replaces stub)
792 -flutter_rust_bridge_codegen generate
793 -
794 -# Verify Rust still compiles cleanly
795 -cd rust && cargo check
796 -
797 -# Verify Dart analysis is still clean
798 -cd .. && dart analyze lib/
799 -```
800 -
801 ----
802 -
803 -## `flutter run` on iOS (Full Steps)
804 -
805 -```bash
806 -# 1. Scaffold the example Flutter project (one-time)
807 -cd sdk/dart/example
808 -flutter create . \
809 - --project-name <example_name> \
810 - --platforms=ios,macos \
811 - --org com.<your_org>
812 -
813 -# 2. Get dependencies
814 -flutter pub get
815 -
816 -# 3. Run codegen (from the SDK package root, not example root)
817 -cd ..
818 -flutter_rust_bridge_codegen generate
819 -
820 -# 4. Run the app (from example dir)
821 -cd example
822 -flutter run # picks up connected device/simulator automatically
823 -flutter run -d iPhone # target a specific device
824 -```
825 -
826 -**What happens during the first `flutter run` on iOS:**
827 -1. Flutter invokes `pod install` → reads `ios/onde_inference.podspec`
828 -2. The podspec script phase runs `cargo build --release --target aarch64-apple-ios-sim`
829 - (or `-device`) inside `sdk/dart/rust/` — this takes 5–15 min first time
830 -3. The resulting `libonde_inference_dart.a` is linked into the Runner app
831 -4. Flutter's Dart VM starts, calls `RustLib.init()`, loads the native library
832 -5. The model downloads from HuggingFace Hub on first inference (~941 MB or ~1.93 GB)
833 -
834 -**HuggingFace token for private/gated models:**
835 -```bash
836 -# Bake token at build time (required for iOS — no filesystem access):
837 -export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
838 -flutter run
839 -```
840 -
841 ----
842 -
843 -## Version Pinning Rule
844 -
845 -The `flutter_rust_bridge` Dart package and `flutter_rust_bridge` Rust crate
846 -**must be the same version**. Use exact pins:
847 -
848 -```toml
849 -# rust/Cargo.toml
850 -flutter_rust_bridge = "=2.12.0"
851 -```
852 -```yaml
853 -# pubspec.yaml
854 -dependencies:
855 - flutter_rust_bridge: ^2.12.0 # ^ is OK on Dart side; codegen auto-upgrades
856 -```
857 -
858 -If they drift, generated code won't compile. Always check both when upgrading.
859 -
860 ----
861 -
862 -## Common Errors and Fixes
863 -
864 -| Error | Cause | Fix |
865 -|-------|-------|-----|
866 -| `rust_input: invalid type: sequence` | YAML list syntax in FRB 2.x | Change to `rust_input: "crate::api"` |
867 -| `Please migrate configuration rust_input` | File path in FRB 2.x | Change to `rust_input: "crate::api"` |
868 -| `File exists (os error 17)` | `frb_generated.rs` pre-exists | `rm rust/src/frb_generated.rs` |
869 -| `could not find package flutter_rust_bridge_codegen` | Added codegen as Dart dep | Remove it; install via `cargo install` |
870 -| `no such command: expand` | `cargo-expand` not installed | `cargo install cargo-expand` |
871 -| `unexpected cfg condition name: frb_expand` | Rust check-cfg lint | Harmless FRB macro warning; suppress with `#[allow(unexpected_cfgs)]` or ignore |
872 -| `no_leading_underscores_for_library_prefixes` | Import alias `as _frb` | Rename to `as frb` |
873 -| `unnecessary_library_name` | `library <name>;` in barrel | Change to bare `library;` |
874 -| `OndeChatEngine.create() throws UnimplementedError` | Codegen not run yet | Run `flutter_rust_bridge_codegen generate` |
875 -| `flutter run` fails, missing Runner.xcodeproj | `flutter create` not run | Run `flutter create . --platforms=ios ...` in example dir |
876 -| `dlopen(onde_inference_dart.framework/...): no such file` | FRB default loader looks for wrong framework | Use `ExternalLibrary.open('onde_inference.framework/onde_inference')` in `init()` — see Dart Public API Pattern above |
877 -| `Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...)` | Used `DynamicLibrary.process()` but symbols are in pod framework, not main executable | Switch from `ExternalLibrary.process()` to `ExternalLibrary.open('onde_inference.framework/onde_inference')` |
878 -| `ld: symbol(s) not found ... _kSCNetworkReachability...` | Missing `SystemConfiguration` framework in podspec | Add `s.frameworks = 'SystemConfiguration'` to macOS podspec |
879 -| `ld: error: unknown argument '-Xlinker'` / `-dynamiclib` / `-filelist` | Android NDK's `ld` (LLD) is on PATH — Xcode picks it up instead of Apple's linker | Remove `export LD=$TOOLCHAIN/bin/ld` and other NDK tool exports from `~/.zshrc` (see Xcode 26 section) |
880 -| `clang: error: invalid linker name in argument '-fuse-ld=classic'` | `ALTERNATE_LINKER = classic` set but clang doesn't support it | Remove `ALTERNATE_LINKER`; was only needed to work around the NDK-on-PATH issue — once that's fixed, Apple's ld-prime works fine |
881 -| `EntityTooLarge` on `dart pub publish` | Package > 100 MB (rust/target/ included) | Create `.pubignore` excluding `rust/target/`, `**/*.a`, `**/*.so`, etc. |
882 -| `Because <pkg> requires SDK version >=X.Y.0 ... version solving failed` | `pubspec.yaml` `environment.sdk` lower bound is too old; CI Flutter version too old | 1. Update `sdk/dart/.flutter-version` to a Flutter that ships the required Dart SDK. 2. Tighten `environment.sdk` in `pubspec.yaml` to the real minimum. See Hard-Won Lesson 11. |
883 -| `You must have a LICENSE file in the root directory` on `dart pub publish` | pub.dev requires a file named exactly `LICENSE`; `LICENSE-MIT` / `LICENSE-APACHE` alone do not satisfy it | Create `sdk/dart/LICENSE` with an **"either of … at your option"** preamble, then append both full license texts. A bare concatenation without a preamble is legally ambiguous — it implies users must comply with *both*, defeating the purpose of dual licensing. Run from `sdk/dart/`: `cat > LICENSE << 'EOF'` then the preamble (see legal-and-trademarks SKILL.md for exact wording), then `{ cat LICENSE-MIT; printf '\n\n---\n\n'; cat LICENSE-APACHE; } >> LICENSE`. Keep `LICENSE-MIT` and `LICENSE-APACHE` alongside it. |
884 -
885 -## Xcode 26 Compatibility
886 -
887 -### The NDK-on-PATH Trap
888 -
889 -If your `~/.zshrc` (or similar) has **global** Android NDK toolchain exports:
890 -
891 -```bash
892 -# DANGEROUS — breaks ALL non-Android builds (Xcode, Rust for Apple, etc.)
893 -export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
894 -export LD=$TOOLCHAIN/bin/ld # ← THIS HIJACKS XCODE'S LINKER
895 -export AR=$TOOLCHAIN/bin/llvm-ar
896 -export CXX=$TOOLCHAIN/bin/clang++
897 -export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
898 -export STRIP=$TOOLCHAIN/bin/llvm-strip
899 -```
900 -
901 -Xcode reads `$LD` and invokes the Android NDK's LLD instead of Apple's linker.
902 -The errors look like Xcode 26 ld-prime incompatibility (`unknown argument '-Xlinker'`)
903 -but the root cause is the **wrong linker binary**.
904 -
905 -**Fix:** Comment out all NDK tool exports. Keep only the env vars:
906 -
907 -```bash
908 -export NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
909 -export ANDROID_NDK="$NDK_HOME"
910 -export ANDROID_NDK_HOME="$NDK_HOME"
911 -# DO NOT export LD, AR, CXX, RANLIB, STRIP globally.
912 -# Cargo uses rust/.cargo/config.toml for Android linker selection.
913 -```
914 -
915 -### Does Xcode 26's ld-prime work with Flutter?
916 -
917 -**Yes** — once the NDK tool exports are removed, Apple's ld-prime handles all
918 -CocoaPods/Flutter linker flags correctly. No `ALTERNATE_LINKER = classic` or
919 -`-ld_classic` workaround is needed for macOS builds. The `-ld_classic`
920 -deprecation warning from iOS Podfiles is harmless.
921 -
922 -### iOS 26 Debug Mode Crash
923 -
924 -Flutter debug builds crash on iOS 26 beta with
925 -`NOTIFY_DEBUGGER_ABOUT_RX_PAGES was not intercepted as expected`
926 -([flutter#170416](https://github.com/flutter/flutter/issues/170416)).
927 -This is a Dart VM JIT issue — **not fixable in the SDK**.
928 -
929 -| Mode | iOS 26 Physical | iOS 26 Simulator | iOS 18.x |
930 -|------|----------------|-----------------|----------|
931 -| `--debug` | ❌ Dart VM crash | ❌ Dart VM crash | ✅ Works |
932 -| `--profile` | ✅ Works | ❌ Not supported on sim | ✅ Works |
933 -| `--release` | ⚠️ Use `flutter build ipa` | ❌ Not supported on sim | ✅ Works |
934 -
935 -**Workaround:** Use an iOS 18.x simulator runtime (download via Xcode Settings → Platforms).
936 -
937 ----
938 -
939 -## File Interdependency Map
940 -
941 -```
942 -pubspec.yaml
943 - └── declares: flutter_rust_bridge ^2.12.0
944 -
945 -flutter_rust_bridge.yaml
946 - ├── rust_root: "rust/" → rust/Cargo.toml
947 - ├── rust_input: "crate::api" → rust/src/api.rs (source of truth)
948 - └── dart_output: "lib/src/frb_generated.dart" (codegen writes here)
949 -
950 -rust/Cargo.toml
951 - ├── crate-type = ["cdylib", "staticlib"]
952 - ├── flutter_rust_bridge = "=2.12.0" (exact pin!)
953 - └── onde = { path = "../../../" } (the main onde crate)
954 -
955 -rust/.cargo/config.toml
956 - └── Android NDK linker overrides (aarch64, armv7, x86_64, i686)
957 -
958 -rust/src/api.rs
959 - ├── imports onde::{ChatEngine, ...} (path dep → ../../..)
960 - ├── declares mirror types (ChatRole, ChatMessage, ...)
961 - ├── declares OndeChatEngine (wraps ChatEngine)
962 - └── declares free functions (default_model_config, ...)
963 -
964 -lib/src/frb_generated.dart/ ← DIRECTORY (not a file!)
965 - ├── api.dart ← generated API bindings
966 - ├── api.freezed.dart ← freezed union types (OndeError)
967 - ├── frb_generated.dart ← RustLib class, ExternalLibraryLoaderConfig
968 - └── frb_generated.io.dart ← RustLibWire, platform FFI glue
969 -
970 -lib/src/types.dart ← hand-written, FRB-independent
971 - └── ChatRole, ChatMessage, SamplingConfig, GgufModelConfig,
972 - InferenceResult, StreamChunk, EngineStatus, EngineInfo, OndeException
973 -
974 -lib/src/engine.dart ← hand-written, imports frb_generated.dart/ + types.dart
975 - ├── extension OndeChatEngineX (loadDefaultModel, clearHistoryCount)
976 - └── abstract class OndeInference
977 - ├── init() → ExternalLibrary.open('onde_inference.framework/...') on Apple
978 - ├── defaultModelConfig(), qwen2515bConfig(), qwen253bConfig(), ...
979 - └── defaultSamplingConfig(), deterministicSamplingConfig(), mobileSamplingConfig()
980 -
981 -lib/onde_inference.dart (barrel)
982 - ├── export 'src/types.dart'
983 - └── export 'src/engine.dart'
984 -
985 -macos/
986 - ├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
987 - └── onde_inference.podspec ← script_phase + -force_load + SystemConfiguration
988 -
989 -ios/
990 - ├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
991 - └── onde_inference.podspec ← script_phase + -force_load
992 -
993 -android/
994 - ├── CMakeLists.txt ← invokes cargo build per ABI
995 - └── build.gradle ← externalNativeBuild → cmake
996 -
997 -.pubignore ← excludes rust/target/, *.a, *.so, etc.
998 -.gitignore ← excludes same + build artifacts
999 -```
1000 -
1001 ----
1002 -
1003 -*Last updated: July 2025 — Xcode 26 compatibility, ExternalLibrary.open pattern,
1004 -SystemConfiguration framework, .pubignore for pub.dev, NDK PATH pitfall.*
\ No newline at end of file
.agents/skills/sdk-kotlin/SKILL.md deleted
-810
@@ -1,810 +0,0 @@
1 ----
2 -name: sdk-kotlin
3 -description: Build, generate, and publish the Onde Inference Kotlin Multiplatform SDK (Android + JVM). Covers UniFFI Kotlin binding generation, KMP source set layout with shared srcDir pattern, cargo-ndk cross-compilation for Android ABIs, JVM native library bundling for macOS Apple Silicon, Maven Central publishing via Vanniktech plugin, klibs.io discovery requirements, ProGuard consumer rules, and the Jetpack Compose example app.
4 -allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5 -user-invocable: true
6 ----
7 -
8 -# Skill: Kotlin Multiplatform SDK
9 -
10 -## What This Skill Covers
11 -
12 -Building the `onde-inference` Kotlin Multiplatform library from Rust source
13 -using UniFFI, distributing it via Maven Central, and getting it listed on
14 -[klibs.io](https://klibs.io) — the JetBrains-maintained discovery platform for
15 -KMP libraries.
16 -
17 -**Targets:**
18 -
19 -| Target | GPU Backend | Default Model | Primary Use Case |
20 -|--------|-------------|---------------|------------------|
21 -| `androidTarget()` | CPU (Candle) | Qwen 2.5 1.5B (~941 MB) | Mobile apps |
22 -| `jvm()` | Metal (macOS) / CPU (Linux) | Qwen 2.5 3B (~1.93 GB) | Desktop / server on Apple Silicon |
23 -
24 -**Context:** Onde's tagline is "AI for Apple silicon devices." The primary SDKs
25 -are **Swift** (iOS, tvOS, macOS) and **Dart** (Flutter, cross-platform). The
26 -Kotlin SDK extends Onde to Android and JVM developers — particularly those
27 -building KMP apps that target both Android and macOS, or running inference on
28 -Apple Silicon Macs from Kotlin/JVM.
29 -
30 ----
31 -
32 -## Repository Layout
33 -
34 -```
35 -onde/
36 -├── src/ # Rust source — UniFFI-annotated types and OndeChatEngine
37 -├── uniffi-bindgen/ # Standalone uniffi-bindgen CLI binary (pinned =0.31.0)
38 -├── sdk/kotlin/
39 -│ ├── settings.gradle.kts # Gradle root — includes :lib and :example
40 -│ ├── build.gradle.kts # root — plugin declarations (all versions live here)
41 -│ ├── gradle.properties # GROUP, POM_ARTIFACT_ID, VERSION_NAME, signing/OSSRH
42 -│ ├── gradle/wrapper/
43 -│ │ └── gradle-wrapper.properties # pinned Gradle 8.9
44 -│ ├── lib/
45 -│ │ ├── build.gradle.kts # KMP library + Vanniktech maven-publish
46 -│ │ ├── consumer-rules.pro # ProGuard: preserve UniFFI JNI bridge symbols
47 -│ │ └── src/
48 -│ │ ├── commonMain/kotlin/.gitkeep # empty — KMP requires this source set to exist
49 -│ │ ├── shared/kotlin/ # shared srcDir added to BOTH androidMain and jvmMain
50 -│ │ │ └── com/ondeinference/onde/
51 -│ │ │ ├── PlatformSupport.kt # internal interface — setEnv + ensureNativeLoaded
52 -│ │ │ ├── OndeInference.kt # engine wrapper (internal constructor)
53 -│ │ │ └── Convenience.kt # OndeSampling, OndeModels, OndeMessage
54 -│ │ ├── generated/kotlin/ # UniFFI-generated onde.kt ← GITIGNORED
55 -│ │ ├── androidMain/
56 -│ │ │ ├── AndroidManifest.xml
57 -│ │ │ ├── jniLibs/ # compiled .so per ABI ← GITIGNORED
58 -│ │ │ └── kotlin/com/ondeinference/onde/
59 -│ │ │ └── Platform.android.kt # AndroidPlatform + factory fun OndeInference(context)
60 -│ │ └── jvmMain/
61 -│ │ ├── kotlin/com/ondeinference/onde/
62 -│ │ │ ├── Platform.jvm.kt # JvmPlatform + factory fun OndeInference(dataDir)
63 -│ │ │ └── NativeLoader.kt # extract libonde from JAR resources
64 -│ │ └── resources/native/ # bundled .dylib/.so ← GITIGNORED
65 -│ │ ├── macos-aarch64/libonde.dylib
66 -│ │ ├── macos-x86_64/libonde.dylib
67 -│ │ ├── linux-x86_64/libonde.so
68 -│ │ └── linux-aarch64/libonde.so
69 -│ ├── example/
70 -│ │ ├── build.gradle.kts # com.android.application — no plugin versions here
71 -│ │ ├── proguard-rules.pro
72 -│ │ ├── README.md
73 -│ │ └── src/main/
74 -│ │ ├── AndroidManifest.xml # INTERNET permission
75 -│ │ ├── res/values/
76 -│ │ │ ├── strings.xml
77 -│ │ │ └── themes.xml # Material3 NoActionBar activity theme
78 -│ │ └── kotlin/com/ondeinference/example/
79 -│ │ ├── MainActivity.kt # ComponentActivity — enableEdgeToEdge + setContent
80 -│ │ ├── ChatViewModel.kt # model lifecycle, streaming, history
81 -│ │ └── ui/
82 -│ │ ├── ChatScreen.kt # full Compose UI
83 -│ │ └── theme/Theme.kt # dynamic colour + dark mode
84 -│ └── scripts/
85 -│ ├── build-android.sh # Rust .so → androidMain/jniLibs/ via cargo-ndk
86 -│ ├── build-jvm.sh # Rust .dylib/.so → jvmMain/resources/native/
87 -│ └── generate-bindings.sh # uniffi-bindgen → src/generated/kotlin/onde.kt
88 -└── .github/workflows/
89 - └── release-sdk-kotlin.yml # tag push → build → Maven Central publish
90 -```
91 -
92 ----
93 -
94 -## KMP Architecture
95 -
96 -### Why KMP Instead of Android-Only
97 -
98 -1. **klibs.io listing** — the JetBrains KMP discovery platform requires
99 - `kotlin-tooling-metadata.json`, which only the `kotlin.multiplatform` Gradle
100 - plugin generates. An Android-only AAR will not appear on klibs.io.
101 -2. **JVM target** — Kotlin developers on macOS Apple Silicon can run inference
102 - locally from desktop/server apps, Gradle plugins, or CLI tools.
103 -3. **Future-proof** — adding Kotlin/Native targets (iOS, macOS) later requires
104 - no structural changes. The Swift SDK already covers Apple platforms natively,
105 - but KMP apps that share Kotlin across Android + iOS could benefit.
106 -
107 -### Source Set Strategy: Shared srcDir
108 -
109 -Instead of KMP intermediate source sets (which add Gradle complexity and
110 -require careful dependency resolution for JNA), the SDK uses a **shared srcDir**
111 -pattern:
112 -
113 -```
114 -commonMain/ — empty (KMP requires it)
115 -shared/ — hand-written wrapper code (compiled for BOTH targets)
116 -generated/ — UniFFI-generated onde.kt (compiled for BOTH targets)
117 -androidMain/ — Android-specific: Os.setenv, Context factory
118 -jvmMain/ — JVM-specific: JNA setenv, NativeLoader, File factory
119 -```
120 -
121 -In `lib/build.gradle.kts`:
122 -
123 -```kotlin
124 -androidMain.get().kotlin.srcDir("src/shared/kotlin")
125 -androidMain.get().kotlin.srcDir("src/generated/kotlin")
126 -jvmMain.get().kotlin.srcDir("src/shared/kotlin")
127 -jvmMain.get().kotlin.srcDir("src/generated/kotlin")
128 -```
129 -
130 -Both targets compile the shared code independently with their own dependencies.
131 -Since both are JVM-based, `java.io.File`, JNA, and coroutines are available in
132 -the shared code.
133 -
134 -### Platform Abstraction: Interface Pattern
135 -
136 -The shared `OndeInference` class takes a `PlatformSupport` interface via an
137 -`internal` constructor. Each target provides a concrete implementation and a
138 -top-level factory function:
139 -
140 -```
141 -PlatformSupport (interface — shared/)
142 -├── AndroidPlatform (object — androidMain/)
143 -│ └── fun OndeInference(context, dataDir?) → OndeInference
144 -└── JvmPlatform (object — jvmMain/)
145 - └── fun OndeInference(dataDir?) → OndeInference
146 -```
147 -
148 -| Operation | Android | JVM |
149 -|-----------|---------|-----|
150 -| `setEnv(key, value)` | `Os.setenv(key, value, true)` | JNA → `libc.setenv(key, value, 1)` |
151 -| `ensureNativeLoaded()` | No-op (APK jniLibs) | `NativeLoader.ensureLoaded()` |
152 -| Default `dataDir` | `context.filesDir` | `~/.onde/` |
153 -
154 ----
155 -
156 -## Key Design Decisions
157 -
158 -| Decision | Rationale |
159 -|---|---|
160 -| KMP with `androidTarget()` + `jvm()` | Required for klibs.io listing; enables macOS Apple Silicon JVM users |
161 -| Shared srcDir instead of intermediate source sets | Avoids KMP dependency resolution complexity with JNA (`@aar` vs JAR) |
162 -| Interface-based platform abstraction | Simpler than `expect`/`actual` when constructor signatures differ between targets |
163 -| UniFFI Kotlin bindings — not hand-written JNI | Same Rust source as Swift/Dart, zero API drift |
164 -| `src/generated/kotlin/` is gitignored | Regenerated from Rust on every release |
165 -| `cargo-ndk` for Android cross-compilation | Handles NDK toolchain wiring for all 4 ABIs transparently |
166 -| `NativeLoader` extracts dylib from JAR resources | JVM users don't need to manage `java.library.path` manually |
167 -| Vanniktech `maven-publish` plugin with `KotlinMultiplatform` | Publishes KMP metadata + per-target artifacts to Maven Central |
168 -| `minSdk 26` | Required for `android.system.Os.setenv` to set `HF_HOME` |
169 -| JNA dependency (`net.java.dev.jna:jna:5.14.0`) | UniFFI 0.31.0 generates Kotlin bindings using `com.sun.jna.*`. Android uses `@aar` variant; JVM uses plain JAR. |
170 -| ProGuard consumer rules bundled in AAR | Prevents R8 from stripping UniFFI JNI bridge in consumer apps |
171 -| `:example` is a Gradle submodule | Plugin versions declared once in root `build.gradle.kts` and inherited |
172 -
173 ----
174 -
175 -## UniFFI Kotlin Type Map
176 -
177 -| Rust type | UniFFI derive | Kotlin type |
178 -|---|---|---|
179 -| `ChatRole` | `uniffi::Enum` | `sealed class ChatRole` |
180 -| `ChatMessage` | `uniffi::Record` | `data class ChatMessage` |
181 -| `SamplingConfig` | `uniffi::Record` | `data class SamplingConfig` |
182 -| `GgufModelConfig` | `uniffi::Record` | `data class GgufModelConfig` |
183 -| `InferenceResult` | `uniffi::Record` | `data class InferenceResult` |
184 -| `StreamChunk` | `uniffi::Record` | `data class StreamChunk` |
185 -| `EngineStatus` | `uniffi::Enum` | `sealed class EngineStatus` |
186 -| `EngineInfo` | `uniffi::Record` | `data class EngineInfo` |
187 -| `InferenceError` | `uniffi::Error` | `sealed class InferenceException : Exception` |
188 -| `OndeChatEngine` | `uniffi::Object` | `class OndeChatEngine` |
189 -| `StreamChunkListener` | `callback_interface` | `interface StreamChunkListener` |
190 -
191 -The generated Kotlin package is `uniffi.onde.*`. The shared `OndeInference.kt`
192 -re-exports these as `typealias` under `com.ondeinference.onde.*` so callers only
193 -need one import.
194 -
195 ----
196 -
197 -## Build Targets
198 -
199 -### Android ABI Targets
200 -
201 -| ABI | Rust triple | Typical device |
202 -|---|---|---|
203 -| `arm64-v8a` | `aarch64-linux-android` | Modern Android phones (primary) |
204 -| `armeabi-v7a` | `armv7-linux-androideabi` | Older 32-bit ARM phones |
205 -| `x86_64` | `x86_64-linux-android` | Android emulators (Intel/AMD) |
206 -| `x86` | `i686-linux-android` | 32-bit emulators |
207 -
208 -### JVM Native Library Targets
209 -
210 -| Platform | Rust triple | Library | Primary Use Case |
211 -|---|---|---|---|
212 -| macOS Apple Silicon | `aarch64-apple-darwin` | `libonde.dylib` | **Primary** — Mac development + Metal GPU |
213 -| macOS Intel | `x86_64-apple-darwin` | `libonde.dylib` | Legacy Mac support |
214 -| Linux x86_64 | `x86_64-unknown-linux-gnu` | `libonde.so` | CI, servers |
215 -| Linux ARM64 | `aarch64-unknown-linux-gnu` | `libonde.so` | ARM servers |
216 -
217 ----
218 -
219 -## Build Sequence (Manual / CI)
220 -
221 -### 1. Prerequisites
222 -
223 -```bash
224 -# Rust toolchain
225 -rustup toolchain install stable
226 -
227 -# Android targets + cargo-ndk
228 -rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
229 -cargo install cargo-ndk
230 -
231 -# Android NDK
232 -export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/26.1.10909125
233 -
234 -# macOS/JVM target (usually pre-installed on Apple Silicon)
235 -rustup target add aarch64-apple-darwin
236 -```
237 -
238 -### 2. Build Android .so files
239 -
240 -```bash
241 -./sdk/kotlin/scripts/build-android.sh # release, all ABIs
242 -./sdk/kotlin/scripts/build-android.sh --debug # debug build
243 -FILTER_ABI=arm64-v8a ./sdk/kotlin/scripts/build-android.sh # single ABI
244 -```
245 -
246 -Outputs land in `sdk/kotlin/lib/src/androidMain/jniLibs/<ABI>/libonde.so`.
247 -
248 -### 3. Build JVM native library
249 -
250 -```bash
251 -./sdk/kotlin/scripts/build-jvm.sh # host platform, release
252 -./sdk/kotlin/scripts/build-jvm.sh --debug # debug build
253 -ONDE_TARGET_TRIPLE=x86_64-apple-darwin ./sdk/kotlin/scripts/build-jvm.sh # cross-compile
254 -```
255 -
256 -Outputs land in `sdk/kotlin/lib/src/jvmMain/resources/native/<os-arch>/libonde.<ext>`.
257 -
258 -### 4. Build the uniffi-bindgen CLI
259 -
260 -```bash
261 -cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
262 -# Binary: uniffi-bindgen/target/release/uniffi-bindgen
263 -```
264 -
265 -### 5. Generate Kotlin UniFFI bindings
266 -
267 -```bash
268 -./sdk/kotlin/scripts/generate-bindings.sh
269 -```
270 -
271 -Produces `sdk/kotlin/lib/src/generated/kotlin/onde.kt` — the complete
272 -generated binding including all types, the `OndeChatEngine` class, free
273 -functions, and `System.loadLibrary("onde")` in a companion object static init.
274 -
275 -### 6. Build the library
276 -
277 -```bash
278 -cd sdk/kotlin
279 -
280 -# Android AAR
281 -./gradlew :lib:assembleRelease
282 -
283 -# JVM JAR (with bundled native lib)
284 -./gradlew :lib:jvmJar
285 -
286 -# Both
287 -./gradlew :lib:build
288 -```
289 -
290 ----
291 -
292 -## JVM Native Library Loading
293 -
294 -The JVM target bundles native libraries inside the JAR under
295 -`/native/<os>-<arch>/libonde.<ext>`. The `NativeLoader` object handles
296 -extraction and loading at runtime:
297 -
298 -```
299 -OndeInference() factory (jvmMain)
300 - └── JvmPlatform.ensureNativeLoaded()
301 - └── NativeLoader.ensureLoaded()
302 - ├── Detect OS + arch from system properties
303 - ├── Extract /native/<os>-<arch>/libonde.<ext> from JAR resources
304 - ├── Write to temp file: <java.io.tmpdir>/onde-native/libonde.<ext>
305 - └── System.load(tempFile.absolutePath)
306 -```
307 -
308 -If the bundled library is not found (e.g. development mode without running
309 -`build-jvm.sh`), `NativeLoader` falls back to `System.loadLibrary("onde")`
310 -which searches `java.library.path`.
311 -
312 -**Important:** `NativeLoader.ensureLoaded()` must run BEFORE any UniFFI type is
313 -accessed. The `OndeInference` constructor handles this via the `init` block:
314 -
315 -```kotlin
316 -class OndeInference internal constructor(...) : AutoCloseable {
317 - init { platform.ensureNativeLoaded() } // runs before engine = OndeChatEngine()
318 - private val engine = uniffi.onde.OndeChatEngine()
319 -}
320 -```
321 -
322 ----
323 -
324 -## Android Filesystem Sandbox Setup
325 -
326 -On Android, `dirs::home_dir()` (used by `hf-hub` under the hood) panics because
327 -there is no home directory in the Android sandbox. The Rust engine requires
328 -`HF_HOME` to be set **before** any model load on Android.
329 -
330 -The `OndeInference` wrapper handles this automatically in `setup()`:
331 -
332 -```kotlin
333 -fun setup() {
334 - if (configured) return
335 -
336 - val hfHome = File(dataDir, "models").also { it.mkdirs() }
337 - val hfHubCache = File(hfHome, "hub").also { it.mkdirs() }
338 - val tmpDir = File(dataDir, "tmp").also { it.mkdirs() }
339 -
340 - platform.setEnv("HF_HOME", hfHome.absolutePath)
341 - platform.setEnv("HF_HUB_CACHE", hfHubCache.absolutePath)
342 - platform.setEnv("HUGGINGFACE_HUB_CACHE", hfHubCache.absolutePath)
343 - platform.setEnv("TMPDIR", tmpDir.absolutePath)
344 -
345 - configured = true
346 -}
347 -```
348 -
349 -`setup()` is called automatically by `loadDefaultModel()` and `loadModel()`.
350 -
351 -### Filesystem layout
352 -
353 -```
354 -# Android (context.filesDir)
355 -<filesDir>/
356 -├── models/ ← HF_HOME
357 -│ └── hub/ ← HF_HUB_CACHE
358 -└── tmp/ ← TMPDIR
359 -
360 -# JVM (defaults to ~/.onde/)
361 -~/.onde/
362 -├── models/ ← HF_HOME
363 -│ └── hub/ ← HF_HUB_CACHE
364 -└── tmp/ ← TMPDIR
365 -```
366 -
367 -### Cross-platform model cache comparison
368 -
369 -| Platform | SDK | Cache Location | Shared across apps? |
370 -|---|---|---|---|
371 -| iOS / tvOS / macOS | Swift | App Group container | Yes (same team + entitlement) |
372 -| macOS (JVM) | Kotlin | `~/.onde/models/hub/` | Yes (all JVM apps on same user) |
373 -| Android | Kotlin | `<filesDir>/models/hub/` | No (per-app sandbox) |
374 -| Flutter | Dart | Platform-dependent | Follows platform conventions |
375 -
376 -### Why Android can't share models across apps
377 -
378 -Android's security model assigns each app a unique Linux UID at install time.
379 -The kernel enforces file permissions — app A cannot open a file descriptor
380 -inside app B's `/data/data/com.B/files/`. There is no platform primitive to
381 -override this the way iOS App Groups do. The approaches that don't work:
382 -
383 -- **`android:sharedUserId`** — deprecated API 29, removed API 33
384 -- **`MANAGE_EXTERNAL_STORAGE`** — Google Play rejects unless file manager
385 -- **SAF (Storage Access Framework)** — requires user interaction every time
386 -
387 -Each Onde-powered Android app downloads its own copy (~941 MB). This matches
388 -how TensorFlow Lite, MediaPipe, and ML Kit handle model caching. The SDK caches
389 -after first download — bandwidth cost is one-time per app.
390 -
391 ----
392 -
393 -## Kotlin Public API
394 -
395 -### `OndeInference` — primary entry point
396 -
397 -```kotlin
398 -import com.ondeinference.onde.OndeInference
399 -import com.ondeinference.onde.OndeSampling
400 -import com.ondeinference.onde.OndeModels
401 -import com.ondeinference.onde.OndeMessage
402 -
403 -// ── Android ──────────────────────────────────────────────────
404 -val onde = OndeInference(context) // pass applicationContext
405 -
406 -// ── JVM (macOS / Linux) ──────────────────────────────────────
407 -val onde = OndeInference() // uses ~/.onde/
408 -val onde = OndeInference(File("/custom")) // custom cache dir
409 -
410 -// ── Shared API (both platforms) ──────────────────────────────
411 -
412 -// Load (downloads from HuggingFace Hub on first run)
413 -val elapsed: Double = onde.loadDefaultModel(
414 - systemPrompt = "You are helpful.",
415 - sampling = OndeSampling.mobile()
416 -)
417 -
418 -// Chat (suspend)
419 -val result = onde.chat("What is Rust?")
420 -println(result.text)
421 -
422 -// Streaming (Flow)
423 -onde.stream("Tell me a story.").collect { chunk ->
424 - print(chunk.delta)
425 -}
426 -
427 -// One-shot (no history side-effects)
428 -val summary = onde.generate(
429 - messages = listOf(
430 - OndeMessage.system("Summarise this."),
431 - OndeMessage.user(longText)
432 - )
433 -)
434 -
435 -// Status
436 -val info = onde.info() // EngineInfo
437 -
438 -// History
439 -onde.clearHistory()
440 -onde.pushHistory(OndeMessage.user("Injected context"))
441 -
442 -// Cleanup
443 -onde.unload()
444 -onde.close() // decrements Rust Arc refcount
445 -```
446 -
447 -### `OndeSampling` — convenience constructors
448 -
449 -```kotlin
450 -OndeSampling.default() // temp=0.7, top_p=0.95, max=512 tokens
451 -OndeSampling.deterministic() // temp=0.0, greedy
452 -OndeSampling.mobile() // temp=0.7, max=128 tokens
453 -```
454 -
455 -### `OndeModels` — model config constructors
456 -
457 -```kotlin
458 -OndeModels.default() // platform default (1.5B on Android, 3B on macOS JVM)
459 -OndeModels.qwen25_1_5b() // ~941 MB
460 -OndeModels.qwen25_3b() // ~1.93 GB
461 -```
462 -
463 -### `OndeMessage` — message constructors
464 -
465 -```kotlin
466 -OndeMessage.system("You are helpful.")
467 -OndeMessage.user("Hello!")
468 -OndeMessage.assistant("Hi there!")
469 -```
470 -
471 ----
472 -
473 -## lib/build.gradle.kts Key Sections
474 -
475 -```kotlin
476 -import com.vanniktech.maven.publish.KotlinMultiplatform
477 -import com.vanniktech.maven.publish.JavadocJar
478 -import com.vanniktech.maven.publish.SonatypeHost
479 -
480 -plugins {
481 - id("org.jetbrains.kotlin.multiplatform")
482 - id("com.android.library")
483 - id("com.vanniktech.maven.publish")
484 -}
485 -
486 -kotlin {
487 - androidTarget {
488 - publishLibraryVariants("release")
489 - compilations.all { kotlinOptions { jvmTarget = "17" } }
490 - }
491 - jvm {
492 - compilations.all { kotlinOptions { jvmTarget = "17" } }
493 - }
494 -
495 - sourceSets {
496 - androidMain.dependencies {
497 - implementation("net.java.dev.jna:jna:5.14.0@aar")
498 - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
499 - }
500 - jvmMain.dependencies {
501 - implementation("net.java.dev.jna:jna:5.14.0")
502 - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
503 - }
504 -
505 - // Shared srcDirs — compiled independently for each target
506 - androidMain.get().kotlin.srcDir("src/shared/kotlin")
507 - androidMain.get().kotlin.srcDir("src/generated/kotlin")
508 - jvmMain.get().kotlin.srcDir("src/shared/kotlin")
509 - jvmMain.get().kotlin.srcDir("src/generated/kotlin")
510 - }
511 -}
512 -
513 -mavenPublishing {
514 - configure(KotlinMultiplatform(
515 - javadocJar = JavadocJar.Empty(),
516 - sourcesJar = true,
517 - ))
518 - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
519 - signAllPublications()
520 - coordinates("com.ondeinference", "onde-inference", "<version>")
521 -}
522 -```
523 -
524 -The Vanniktech plugin reads signing and OSSRH credentials from environment
525 -variables prefixed with `ORG_GRADLE_PROJECT_`:
526 -
527 -| Gradle property | CI env var |
528 -|---|---|
529 -| `mavenCentralUsername` | `ORG_GRADLE_PROJECT_mavenCentralUsername` |
530 -| `mavenCentralPassword` | `ORG_GRADLE_PROJECT_mavenCentralPassword` |
531 -| `signingKeyId` | `ORG_GRADLE_PROJECT_signingKeyId` |
532 -| `signingKey` | `ORG_GRADLE_PROJECT_signingKey` |
533 -| `signingPassword` | `ORG_GRADLE_PROJECT_signingPassword` |
534 -
535 ----
536 -
537 -## ProGuard Consumer Rules
538 -
539 -The `consumer-rules.pro` bundled in the AAR instructs R8/ProGuard in consumer
540 -apps to preserve:
541 -
542 -- `uniffi.onde.**` — all generated binding classes
543 -- `com.ondeinference.onde.**` — the public wrapper
544 -- `uniffi.onde.StreamChunkListener` — callback interface called from Rust
545 -- Native method declarations (`keepclasseswithmembernames`)
546 -- Static initializers in `uniffi.onde.*` (contains `System.loadLibrary("onde")`)
547 -
548 -Always update `consumer-rules.pro` when adding new top-level UniFFI-exported
549 -types or callback interfaces.
550 -
551 ----
552 -
553 -## klibs.io Discovery
554 -
555 -[klibs.io](https://klibs.io) is a JetBrains-maintained search platform for
556 -Kotlin Multiplatform libraries. It auto-indexes from Maven Central.
557 -
558 -### Requirements for listing
559 -
560 -| Requirement | How we satisfy it |
561 -|---|---|
562 -| Open source on GitHub | `ondeinference/onde` is public |
563 -| At least one artifact on Maven Central | Published via Vanniktech plugin |
564 -| Artifact has `kotlin-tooling-metadata.json` | KMP plugin generates this automatically |
565 -| POM has valid GitHub `url` or `scm.url` | `scm { url.set("https://github.com/ondeinference/onde") }` |
566 -
567 -### Timeline
568 -
569 -- New libraries appear **within one month** of first Maven Central publish
570 - (public index update frequency).
571 -- New versions of already-indexed libraries appear **the next day**.
572 -- No manual submission needed — discovery is fully automatic.
573 -
574 ----
575 -
576 -## Maven Central Publishing (CI)
577 -
578 -Trigger: push a semver tag matching `[0-9]+.[0-9]+.[0-9]+`.
579 -
580 -```bash
581 -# 1. Bump VERSION_NAME in sdk/kotlin/gradle.properties
582 -# 2. Bump version in Cargo.toml [package]
583 -# 3. Commit, tag, push
584 -git tag 1.0.1 && git push origin 1.0.1
585 -```
586 -
587 -CI (`release-sdk-kotlin.yml`) steps:
588 -1. Install Rust stable with all 4 Android targets + host target
589 -2. Cache NDK 26.1.10909125
590 -3. Install `cargo-ndk`
591 -4. `build-android.sh --release` → `.so` files
592 -5. `build-jvm.sh --release` → `.dylib` / `.so`
593 -6. Build `uniffi-bindgen`
594 -7. `generate-bindings.sh` → `onde.kt`
595 -8. Validate tag == `Cargo.toml` version
596 -9. `./gradlew :lib:publishAndReleaseToMavenCentral`
597 -10. Upload artifacts as GitHub Release assets
598 -
599 ----
600 -
601 -## Version Synchronisation Rule
602 -
603 -`VERSION_NAME` in `sdk/kotlin/gradle.properties` MUST match `version` in the
604 -root `Cargo.toml`. The CI workflow enforces this and fails fast on mismatch.
605 -Never bump one without the other.
606 -
607 ----
608 -
609 -## Common Pitfalls
610 -
611 -| Pitfall | Fix |
612 -|---|---|
613 -| `dirs::home_dir()` panics on Android | `setup()` must be called before any Rust model load. `OndeInference.loadDefaultModel()` calls it automatically. |
614 -| Generated `onde.kt` not found at compile time | Run `scripts/generate-bindings.sh`. The file is gitignored and must be regenerated after every Rust API change. |
615 -| `.so` files missing from AAR | Run `scripts/build-android.sh`. The `androidMain/jniLibs/` directory is gitignored. |
616 -| JVM `UnsatisfiedLinkError` at runtime | Run `scripts/build-jvm.sh` to bundle the native lib in JAR resources, or add `libonde.dylib` to `java.library.path`. |
617 -| UniFFI version mismatch | Keep `uniffi = "=0.31.0"` identical in `Cargo.toml`, `build-deps`, and `uniffi-bindgen/Cargo.toml`. |
618 -| R8 strips UniFFI bridge at runtime | Ensure `consumer-rules.pro` is present in the AAR and includes `-keep class uniffi.onde.**`. |
619 -| `System.loadLibrary("onde")` fails | The `.so` files must match the ABI of the device. Check that all 4 ABIs are built and included in `androidMain/jniLibs/`. |
620 -| `Os.setenv` not available | `minSdk` must be 26+. `android.system.Os` is available from API 21 but `setenv` specifically needs 26. |
621 -| Gradle fails with `VERSION_NAME` missing | Ensure `gradle.properties` contains `VERSION_NAME=x.y.z`. |
622 -| `cargo-ndk` not found in CI | Add `cargo install cargo-ndk --locked` step before the build step. |
623 -| `ANDROID_NDK_HOME` not set | Set it explicitly: `echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/26.1.10909125" >> $GITHUB_ENV` |
624 -| `build-android.sh: line XX: arm64: unbound variable` on macOS | Do not use Bash associative arrays. The script uses portable indexed arrays for ABI → Rust target mapping. |
625 -| `Unresolved reference 'sun'` / `Unresolved reference 'Structure'` in `onde.kt` | UniFFI 0.31.0 depends on JNA (`com.sun.jna.*`). Add `implementation("net.java.dev.jna:jna:5.14.0@aar")` for Android or `implementation("net.java.dev.jna:jna:5.14.0")` for JVM. |
626 -| HuggingFace download fails on first load | The app must declare `<uses-permission android:name="android.permission.INTERNET" />`. |
627 -| `Plugin [id: 'com.android.application'] was not found` in `:example` | Open `sdk/kotlin/` (not `sdk/kotlin/example/`) in Android Studio. Plugin versions are inherited from the root `build.gradle.kts`. |
628 -| `Using singleVariant publishing DSL multiple times` | This error means the old `AndroidSingleVariantLibrary` config is still present. Use `KotlinMultiplatform(...)` instead — the KMP plugin handles variant publishing internally. |
629 -| Convenience functions (`OndeSampling.default()` etc.) crash before `OndeInference()` created | These functions call into the native library via UniFFI. Always create an `OndeInference` instance first — its `init` block ensures the native library is loaded. |
630 -
631 ----
632 -
633 -## Testing
634 -
635 -### Unit tests (host JVM — no Android device needed)
636 -
637 -Pure Kotlin logic tests that don't call into Rust:
638 -
639 -```bash
640 -cd sdk/kotlin
641 -./gradlew :lib:test # all JVM unit tests
642 -./gradlew :lib:jvmTest # JVM target only
643 -```
644 -
645 -### Instrumented tests (requires Android device or emulator)
646 -
647 -Tests that call into Rust via the generated bindings:
648 -
649 -```bash
650 -./gradlew :lib:connectedAndroidTest
651 -```
652 -
653 -For CI, use an emulator (`x86_64` ABI) with `reactivecircus/android-emulator-runner`.
654 -
655 -### JVM integration tests
656 -
657 -Tests that load the native library and run inference:
658 -
659 -```bash
660 -# Build the native lib first
661 -./sdk/kotlin/scripts/build-jvm.sh
662 -
663 -# Run JVM tests
664 -cd sdk/kotlin && ./gradlew :lib:jvmTest
665 -```
666 -
667 ----
668 -
669 -## Dependency Graph
670 -
671 -```
672 -OndeInference.kt (shared)
673 -├── PlatformSupport (interface, shared)
674 -│ ├── AndroidPlatform (androidMain)
675 -│ │ └── android.system.Os.setenv
676 -│ └── JvmPlatform (jvmMain)
677 -│ ├── JNA → libc.setenv
678 -│ └── NativeLoader → extracts libonde from JAR resources
679 -├── uniffi.onde.OndeChatEngine (generated onde.kt)
680 -├── uniffi.onde.StreamChunkListener (generated onde.kt)
681 -├── uniffi.onde.*Config / *Message (generated onde.kt)
682 -│ └── com.sun.jna.Native (JNA)
683 -│ ├── libonde.so (Android, per ABI)
684 -│ └── libonde.dylib (macOS, from JAR resources)
685 -│ └── mistralrs (Metal on macOS, Candle CPU on Android)
686 -│ └── hf-hub (model download)
687 -└── Convenience.kt (shared)
688 - └── OndeSampling, OndeModels, OndeMessage
689 -```
690 -
691 ----
692 -
693 -## Distribution Registry
694 -
695 -| Registry | Artifact | Import | Discovery |
696 -|---|---|---|---|
697 -| Maven Central | `com.ondeinference:onde-inference` | `implementation("com.ondeinference:onde-inference:1.0.0")` | [search.maven.org](https://search.maven.org) |
698 -| klibs.io | `onde-inference` | Same Gradle coordinate | [klibs.io](https://klibs.io) (auto-indexed) |
699 -| GitHub Releases | `onde-inference-<version>.aar` + `.jar` | Direct download | [github.com/ondeinference/onde/releases](https://github.com/ondeinference/onde/releases) |
700 -
701 -### End-game for Kotlin developers
702 -
703 -One line in `build.gradle.kts`:
704 -
705 -```kotlin
706 -implementation("com.ondeinference:onde-inference:1.0.0")
707 -```
708 -
709 ----
710 -
711 -## Example App
712 -
713 -A working Jetpack Compose chat app lives at `sdk/kotlin/example/`. It is the
714 -canonical way to see the SDK in action and the first thing to open when
715 -verifying a new build.
716 -
717 -### Opening in Android Studio
718 -
719 -Always open **`sdk/kotlin/`** — the Gradle root. Do not open
720 -`sdk/kotlin/example/` directly. Android Studio will see both `:lib` and
721 -`:example` as modules and sync correctly.
722 -
723 -### What the example covers
724 -
725 -- Loading the default model with `loadDefaultModel()` and showing load time
726 -- Streaming replies token-by-token via `onde.stream()` and `Flow<StreamChunk>`
727 -- Multi-turn conversation history maintained in the Rust engine
728 -- Clearing history without reloading the model
729 -- Material3 dynamic colour theme with dark mode support
730 -- Proper IME / navigation-bar inset handling
731 -
732 -### Gradle submodule rules
733 -
734 -1. **No `settings.gradle.kts` inside `example/`** — Gradle would treat it as
735 - standalone, breaking plugin resolution and `project(":lib")`.
736 -2. **No plugin versions in `example/build.gradle.kts`** — inherited from root.
737 -3. **`include(":example")` in root `settings.gradle.kts`** — required for Gradle.
738 -4. **`project(":lib")` as the SDK dependency** — builds against local source.
739 -
740 -The root `build.gradle.kts` declares all plugins used by any submodule:
741 -
742 -```kotlin
743 -plugins {
744 - id("com.android.library") version "8.5.2" apply false
745 - id("com.android.application") version "8.5.2" apply false
746 - id("org.jetbrains.kotlin.android") version "2.0.21" apply false
747 - id("org.jetbrains.kotlin.multiplatform") version "2.0.21" apply false
748 - id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
749 - id("com.vanniktech.maven.publish") version "0.28.0" apply false
750 -}
751 -```
752 -
753 -### Streaming pattern
754 -
755 -```kotlin
756 -val placeholderId = System.nanoTime()
757 -_uiState.update { it.copy(messages = it.messages + UiMessage(id = placeholderId, ...)) }
758 -
759 -val buffer = StringBuilder()
760 -onde.stream(text).collect { chunk ->
761 - buffer.append(chunk.delta)
762 - _uiState.update { state ->
763 - state.copy(messages = state.messages.replaceLast(
764 - UiMessage(id = placeholderId, content = buffer.toString(), isStreaming = !chunk.done)
765 - ))
766 - }
767 -}
768 -```
769 -
770 -### Running the example
771 -
772 -```bash
773 -cd sdk/kotlin
774 -
775 -# 1. Build the .so files
776 -./scripts/build-android.sh
777 -
778 -# 2. Generate the Kotlin bindings
779 -./scripts/generate-bindings.sh
780 -
781 -# 3. Install on a connected device
782 -./gradlew :example:installDebug
783 -```
784 -
785 -Or open `sdk/kotlin/` in Android Studio and hit Run on the `:example` configuration.
786 -
787 ----
788 -
789 -## Cross-Platform SDK Comparison
790 -
791 -Onde ships SDKs for four platforms. The Kotlin SDK is one piece of the
792 -distribution strategy:
793 -
794 -| SDK | Primary Platform | GPU Backend | Package Manager | Discovery |
795 -|-----|-----------------|-------------|-----------------|-----------|
796 -| **Swift** | iOS, tvOS, macOS | Metal | Swift Package Manager | Swift Package Index |
797 -| **Dart** | Android, iOS (Flutter) | Platform-dependent | pub.dev | pub.dev search |
798 -| **Kotlin** | Android, JVM (macOS) | Metal (JVM) / CPU (Android) | Maven Central | klibs.io |
799 -| **React Native** | Android, iOS | Platform-dependent | npm | npmjs.com |
800 -
801 -The Swift SDK is the primary distribution — it runs natively on Apple silicon
802 -with Metal acceleration. The Kotlin JVM target provides the same Metal-backed
803 -inference for Kotlin developers on macOS, while the Android target extends
804 -reach to the Android ecosystem.
805 -
806 ----
807 -
808 -*Update this skill when adding KMP targets (e.g. Kotlin/Native for iOS),
809 -changing the source set structure, or modifying the Maven Central publishing
810 -pipeline.*
\ No newline at end of file
.agents/skills/sdk-react-native/SKILL.md deleted
-769
@@ -1,769 +0,0 @@
1 ----
2 -name: sdk-react-native
3 -description: Build, debug, and maintain the Onde Inference React Native / Expo SDK. Covers Expo native module wiring, Metro config for file-linked SDKs, iOS deployment target and Expo autolinking, XCFramework device/simulator split, app group entitlement, HF_HOME/TMPDIR sandbox setup, the native bridge JSON string contract, EngineStatus normalization, and npm publishing.
4 -allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5 -user-invocable: true
6 ----
7 -
8 -# Skill: React Native / Expo SDK
9 -
10 -## What This Skill Covers
11 -
12 -Building and maintaining the `@ondeinference/react-native` Expo native module,
13 -wiring the Rust C FFI bridge to TypeScript, debugging Metro/CocoaPods/Xcode
14 -issues, and publishing to npm.
15 -
16 ----
17 -
18 -## Repository Layout
19 -
20 -```
21 -onde/sdk/react-native/
22 -├── package.json # name: @ondeinference/react-native
23 -├── expo-module.config.json # declares OndeInferenceModule for Expo autolinking
24 -├── tsconfig.json # target ES2020, module commonjs
25 -├── src/
26 -│ ├── index.ts # public API: OndeChatEngine, free functions, types
27 -│ ├── OndeInferenceModule.ts # requireNativeModule("OndeInference") — raw bridge
28 -│ └── types.ts # ChatMessage, SamplingConfig, EngineInfo, etc.
29 -├── build/ # tsc output — gitignored, regenerated by npm run build
30 -├── ios/
31 -│ ├── OndeInference.podspec # CocoaPods spec — vendored_frameworks = XCFramework
32 -│ ├── OndeInferenceModule.swift # Expo Module: Rust C FFI declarations + module def
33 -│ └── rust/
34 -│ ├── OndeReactNative.xcframework # bundles device + simulator slices
35 -│ ├── device/libonde_react_native.a
36 -│ └── simulator/libonde_react_native.a
37 -├── android/ # Android module (not yet wired for Onde)
38 -├── scripts/
39 -│ └── build-rust.sh # builds Rust .a for iOS device + sim, creates XCFramework
40 -└── example/
41 - ├── app.json # Expo config — ios.deploymentTarget, appleTeamId
42 - ├── babel.config.js # babel-preset-expo only
43 - ├── metro.config.js # watchFolders, extraNodeModules, nodeModulesPaths, blockList
44 - ├── package.json # dep: "@ondeinference/react-native": "file:.."
45 - └── ios/
46 - ├── Podfile # uses use_expo_modules!, deployment from Podfile.properties.json
47 - ├── Podfile.properties.json # ios.deploymentTarget: "16.0" — critical, see below
48 - ├── OndeExample/
49 - │ ├── OndeExample.entitlements
50 - │ └── Info.plist
51 - └── Pods/ # gitignored
52 -```
53 -
54 ----
55 -
56 -## Critical Lessons (Read First)
57 -
58 -### 1. Metro `@babel/runtime` resolution with `file:..` local packages
59 -
60 -**Symptom:** `Unable to resolve module @babel/runtime/helpers/asyncToGenerator`
61 -from inside the SDK's `build/index.js`.
62 -
63 -**Root cause:** When Metro processes a file physically located outside the
64 -example app's directory (e.g. in `sdk/react-native/build/`), Babel transforms
65 -`async/await` and injects `require("@babel/runtime/...")`. Metro resolves
66 -modules relative to the file's physical location — but the SDK's own
67 -`node_modules/` is blocked (to avoid react-native version conflicts) and the
68 -example's `node_modules/` is not in the resolution path for files outside the
69 -example root.
70 -
71 -**Fix — always required in `metro.config.js`:**
72 -
73 -```js
74 -// Falls back to the example's node_modules for any file in watched folders
75 -config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")];
76 -```
77 -
78 -Also required: block the SDK's node_modules to avoid react-native version
79 -conflicts:
80 -
81 -```js
82 -config.resolver.blockList = [
83 - new RegExp(
84 - path.resolve(sdkRoot, "node_modules").replace(/\\/g, "\\\\") + "/.*",
85 - ),
86 -];
87 -```
88 -
89 ----
90 -
91 -### 2. Expo autolinking silently skips pods when deployment target is too low
92 -
93 -**Symptom:** `pod install` completes but `OndeInference` is not in
94 -`Podfile.lock`. The app launches but throws "Cannot find native module
95 -'OndeInference'" at runtime.
96 -
97 -**Root cause:** Expo's autolinking Ruby code calls `pod.supports_platform?`
98 -on each discovered pod. The `OndeInference` podspec declares
99 -`s.platforms = { :ios => '16.0' }`. If the project's deployment target
100 -(from `Podfile.properties.json`) is lower (e.g. the Expo default of 15.1),
101 -`supports_platform?` returns false and the pod is **silently skipped** with no
102 -error or warning.
103 -
104 -**Diagnosis:**
105 -
106 -```bash
107 -xcrun simctl get_app_container booted <bundle-id> data
108 -grep "OndeInference" ios/Podfile.lock # if empty → autolinking skipped it
109 -```
110 -
111 -**Fix — three places must all agree on iOS 16.0:**
112 -
113 -1. `sdk/react-native/ios/OndeInference.podspec` — `s.platforms = { :ios => '16.0' }`
114 -2. `example/ios/Podfile.properties.json` — `"ios.deploymentTarget": "16.0"`
115 -3. `example/app.json` → `expo.ios.deploymentTarget: "16.0"`
116 -
117 -And after changing `Podfile.properties.json`, manually patch the Xcode project:
118 -
119 -```bash
120 -sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = 15.1/IPHONEOS_DEPLOYMENT_TARGET = 16.0/g' \
121 - ios/OndeExample.xcodeproj/project.pbxproj
122 -```
123 -
124 -Then run `pod install` again and verify:
125 -
126 -```bash
127 -grep "OndeInference" ios/Podfile.lock # must now appear
128 -```
129 -
130 -**Important:** `expo prebuild --clean` regenerates `Podfile.properties.json`
131 -from `app.json`. After `--clean`, you must also manually re-add
132 -`"ios.deploymentTarget": "16.0"` to `Podfile.properties.json` if it was not
133 -picked up from `app.json`.
134 -
135 ----
136 -
137 -### 3. iOS device vs simulator static library — use an XCFramework
138 -
139 -**Symptom:** Build succeeds for simulator but fails with:
140 -`ld: building for 'iOS-simulator', but linking in object file built for 'iOS'`
141 -
142 -**Root cause:** Both iOS device (`aarch64-apple-ios`) and iOS simulator
143 -(`aarch64-apple-ios-sim`) are arm64, so `lipo` cannot merge them into a fat
144 -binary. They have different Mach-O platform metadata.
145 -
146 -**Wrong approach:** Single `libonde_react_native.a` → works for device OR
147 -simulator but not both.
148 -
149 -**Wrong approach:** `vendored_libraries` with two files sharing the same
150 -filename → CocoaPods reports "conflicting names" error.
151 -
152 -**Correct approach:** XCFramework bundling both slices:
153 -
154 -```bash
155 -# In scripts/build-rust.sh
156 -mkdir -p ios/rust/device ios/rust/simulator
157 -cp target/aarch64-apple-ios/release/libonde_react_native.a ios/rust/device/
158 -cp target/aarch64-apple-ios-sim/release/libonde_react_native.a ios/rust/simulator/
159 -
160 -rm -rf ios/rust/OndeReactNative.xcframework
161 -xcodebuild -create-xcframework \
162 - -library ios/rust/device/libonde_react_native.a \
163 - -library ios/rust/simulator/libonde_react_native.a \
164 - -output ios/rust/OndeReactNative.xcframework
165 -```
166 -
167 -Podspec uses `vendored_frameworks` (not `vendored_libraries`):
168 -
169 -```ruby
170 -s.vendored_frameworks = 'rust/OndeReactNative.xcframework'
171 -s.pod_target_xcconfig = {
172 - 'DEFINES_MODULE' => 'YES',
173 - 'SWIFT_COMPILATION_MODE' => 'wholemodule',
174 - 'OTHER_LDFLAGS' => '-londe_react_native',
175 -}
176 -```
177 -
178 -CocoaPods automatically selects the correct slice for device vs simulator.
179 -
180 ----
181 -
182 -### 4. The native bridge JSON string contract — never return Swift dicts
183 -
184 -**Symptom:** `JSON parse error: unexpected character: o`
185 -
186 -**Root cause:** The TypeScript layer receives the native return value and calls
187 -`JSON.parse(value)`. If the native Swift module returns `[String: Any]` or
188 -`[[String: Any]]` instead of a `String`, the Expo bridge serialises it in an
189 -opaque way that is not valid JSON (the string representation starts with
190 -something like `{...}` or `[...]` in a non-JSON form that starts with `o`
191 -when printed).
192 -
193 -**The contract:** All Expo Module methods in `OndeInferenceModule.swift` that
194 -return complex types MUST return `String` (the raw JSON from Rust), NOT
195 -`[String: Any]` or `[[String: Any]]`. The TypeScript layer owns all JSON
196 -parsing.
197 -
198 -**Correct Swift:**
199 -
200 -```swift
201 -AsyncFunction("loadDefaultModel") {
202 - (systemPrompt: String?, samplingJson: String?) -> String in // ← String, not [String: Any]
203 - ...
204 - return try self.validateJsonResult(result) // returns the raw JSON string
205 -}
206 -
207 -Function("defaultModelConfig") { () -> String in // ← String, not [String: Any]
208 - let result = self.consumeRustString(onde_default_model_config())
209 - return try self.validateJsonResult(result)
210 -}
211 -```
212 -
213 -**Validation helper — validate but do not deserialise:**
214 -
215 -```swift
216 -private func validateJsonResult(_ json: String?) throws -> String {
217 - guard let json = json,
218 - let data = json.data(using: .utf8),
219 - let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any]
220 - else { throw OndeError.invalidResponse }
221 - if let error = dict["error"] as? String {
222 - throw OndeError.inferenceError(error)
223 - }
224 - return json // ← return the original string, not the parsed dict
225 -}
226 -
227 -private func validateJsonArray(_ json: String?) throws -> String {
228 - guard let json = json,
229 - let data = json.data(using: .utf8),
230 - (try JSONSerialization.jsonObject(with: data) as? [[String: Any]]) != nil
231 - else { throw OndeError.invalidResponse }
232 - return json // ← return the original string
233 -}
234 -```
235 -
236 ----
237 -
238 -### 5. EngineStatus is lowercase from Rust — normalize in the UI layer
239 -
240 -**Symptom:** Model loads successfully. Alert says "Ready in X.Xs". But the
241 -`TextInput` placeholder stays at "Load a model first" and remains non-editable.
242 -
243 -**Root cause:** Rust's `Display` impl emits lowercase status strings:
244 -`"ready"`, `"unloaded"`, `"loading"`, `"generating"`, `"error"`.
245 -The TypeScript UI uses capitalized union type values (`"Ready"`, `"Unloaded"`,
246 -etc.). After `refreshStatus()` runs, `engineStatus` holds `"ready"` which
247 -does not match `"Ready"` in the `isLoaded` check.
248 -
249 -**Fix — normalize at the point of receiving native status:**
250 -
251 -```typescript
252 -const normalizeEngineStatus = useCallback((status: string): EngineStatus => {
253 - switch (status.toLowerCase()) {
254 - case "loading": return "Loading";
255 - case "ready": return "Ready";
256 - case "generating": return "Generating";
257 - case "error": return "Error";
258 - default: return "Unloaded";
259 - }
260 -}, []);
261 -
262 -const refreshStatus = useCallback(async () => {
263 - try {
264 - const info = await OndeChatEngine.info();
265 - setEngineStatus(normalizeEngineStatus(info.status)); // ← normalize here
266 - setModelName(info.modelName ?? null);
267 - setApproxMemory(info.approxMemory ?? null);
268 - } catch { /* best-effort */ }
269 -}, [normalizeEngineStatus]);
270 -```
271 -
272 ----
273 -
274 -### 6. iOS sandbox filesystem setup — HF_HOME must be set before model load
275 -
276 -**Symptom:** Model load fails immediately with "error 0" or returns an error
277 -containing "HF_HOME is not set".
278 -
279 -**Root cause:** On iOS, `~/.cache` is outside the app sandbox. The Rust engine
280 -explicitly requires `HF_HOME` to be set before any model load on sandboxed
281 -platforms. Without it, `load_gguf_model` returns an `InferenceError::ModelBuild`
282 -with the message: *"HF_HOME is not set — cannot initialise HF cache."*
283 -
284 -**Fix — call `configureApplicationFilesystem()` in `OnCreate` and before each
285 -model load:**
286 -
287 -```swift
288 -private func configureApplicationFilesystem() throws {
289 - let fileManager = FileManager.default
290 -
291 - let containerDirectory: URL
292 - if let appGroupDirectory = fileManager.containerURL(
293 - forSecurityApplicationGroupIdentifier: ondeAppGroupIdentifier
294 - ) {
295 - containerDirectory = appGroupDirectory
296 - } else {
297 - containerDirectory = try fileManager.url(
298 - for: .applicationSupportDirectory,
299 - in: .userDomainMask, appropriateFor: nil, create: true
300 - )
301 - }
302 -
303 - // Convention: <container>/models/hub — matches Tauri and Swift SDKs exactly
304 - let hfHomeDirectory = containerDirectory.appendingPathComponent("models", isDirectory: true)
305 - let hfHubCacheDirectory = hfHomeDirectory.appendingPathComponent("hub", isDirectory: true)
306 - let tempDirectory = containerDirectory.appendingPathComponent("tmp", isDirectory: true)
307 -
308 - try fileManager.createDirectory(at: hfHomeDirectory, withIntermediateDirectories: true)
309 - try fileManager.createDirectory(at: hfHubCacheDirectory, withIntermediateDirectories: true)
310 - try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
311 -
312 - setenv("HF_HOME", hfHomeDirectory.path, 1)
313 - setenv("HF_HUB_CACHE", hfHubCacheDirectory.path, 1)
314 - setenv("HUGGINGFACE_HUB_CACHE", hfHubCacheDirectory.path, 1)
315 - setenv("TMPDIR", tempDirectory.path, 1)
316 -}
317 -```
318 -
319 -Call it in `OnCreate` **and** at the start of `loadDefaultModel`/`loadModel`:
320 -
321 -```swift
322 -OnCreate {
323 - do {
324 - try self.configureApplicationFilesystem()
325 - self.enginePtr = onde_engine_create()
326 - } catch {
327 - self.enginePtr = nil
328 - }
329 -}
330 -
331 -AsyncFunction("loadDefaultModel") { ... -> String in
332 - try self.configureApplicationFilesystem() // ← also here, idempotent
333 - guard let ptr = self.enginePtr else { throw OndeError.engineNotInitialized }
334 - ...
335 -}
336 -```
337 -
338 ----
339 -
340 -### 7. Shared app group cache path convention
341 -
342 -All Onde-powered apps across all SDKs (Tauri, Swift, React Native, Flutter)
343 -share the same HuggingFace cache via the App Group container. The layout is:
344 -
345 -```
346 -<group.com.ondeinference.apps container>/
347 -├── models/ ← HF_HOME
348 -│ └── hub/ ← HF_HUB_CACHE (GGUF files live here)
349 -└── tmp/ ← TMPDIR
350 -```
351 -
352 -**The subdirectory is `models/`, NOT `huggingface/`.**
353 -
354 -Do NOT use `OndeInference/huggingface/hub/` — this was an early mistake that
355 -caused the app to look in a different place than the Tauri SDK, preventing
356 -cross-app model reuse.
357 -
358 -Verifying the shared container is accessible on simulator:
359 -
360 -```bash
361 -xcrun simctl get_app_container booted <bundle-id> groups
362 -# → shows group.com.ondeinference.apps path
363 -
364 -xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps
365 -# → resolves the exact path
366 -
367 -# Check whether models exist in the shared cache
368 -find "$(xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps)/models/hub" \
369 - -name "*.gguf" -o -name "*.part" -o -name "*.lock" | sort
370 -```
371 -
372 ----
373 -
374 -### 8. Entitlements required for iOS
375 -
376 -The example app's `OndeExample.entitlements` must contain:
377 -
378 -```xml
379 -<?xml version="1.0" encoding="UTF-8"?>
380 -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
381 - "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
382 -<plist version="1.0">
383 - <dict>
384 - <key>com.apple.developer.kernel.increased-memory-limit</key>
385 - <true />
386 - <key>com.apple.security.application-groups</key>
387 - <array>
388 - <string>group.com.ondeinference.apps</string>
389 - </array>
390 - </dict>
391 -</plist>
392 -```
393 -
394 -- `increased-memory-limit` — required for ~1 GB+ model loading on iOS
395 -- `com.apple.security.application-groups` — grants access to the shared model cache
396 -
397 -**On physical device:** the App Group must be registered in the Apple Developer
398 -Portal under **Identifiers → App Groups** before Xcode can provision it.
399 -
400 -On the iOS Simulator the entitlement is granted automatically by the simulator
401 -runtime — no provisioning profile needed.
402 -
403 ----
404 -
405 -### 9. FlatList swallows taps — TextInput never gets focus
406 -
407 -**Symptom:** After model loads, the `TextInput` placeholder is correct but
408 -tapping it does nothing. The keyboard never appears.
409 -
410 -**Root cause:** `FlatList` on iOS intercepts touch events and by default
411 -dismisses the keyboard on scroll, which prevents child inputs from receiving
412 -focus when tapping in certain layout configurations.
413 -
414 -**Fix:**
415 -
416 -```tsx
417 -<FlatList
418 - ...
419 - keyboardShouldPersistTaps="handled" // ← prevents FlatList eating taps
420 -/>
421 -```
422 -
423 ----
424 -
425 -### 10. Do not use the iOS Simulator to validate model loading
426 -
427 -The iOS Simulator is unreliable for validating actual LLM model loading with
428 -this stack:
429 -- Metal-backed inference behaves differently in the simulator
430 -- Large GGUF model loading (~941 MB) may hang or fail in the simulator
431 -- Shared app group container behavior can differ
432 -
433 -Use the Simulator for:
434 -- Build verification
435 -- UI layout and interaction
436 -- Entitlement/container path checks (`xcrun simctl get_app_container ...`)
437 -
438 -Use a **physical iPhone running iOS 16+** for:
439 -- Actual model download and load
440 -- Metal inference correctness
441 -- Memory behavior
442 -
443 ----
444 -
445 -### 11. Diagnosing a hung model load
446 -
447 -When the app stays in "Loading" state indefinitely:
448 -
449 -1. **Check the shared container has no stale partial downloads:**
450 -
451 -```bash
452 -# Find .part and .lock files
453 -find "$(xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps)" \
454 - -type f \( -name "*.part" -o -name "*.lock" \) | sort
455 -
456 -# Remove stale partial caches
457 -rm -rf "<container>/models/hub/models--<org>--<model>"
458 -```
459 -
460 -2. **Check the app group resolves:**
461 -
462 -```bash
463 -xcrun simctl get_app_container booted <bundle-id> groups
464 -```
465 -
466 -3. **Check native logs:**
467 -
468 -```bash
469 -xcrun simctl spawn booted log show --style compact --last 5m \
470 - --predicate 'process == "OndeExample" AND (
471 - eventMessage CONTAINS[c] "HF_" OR
472 - eventMessage CONTAINS[c] "models" OR
473 - eventMessage CONTAINS[c] "onde"
474 - )' | tail -100
475 -```
476 -
477 -4. **Verify `
478 -HF_TOKEN` was baked in at Rust build time.** On iOS,
479 -`TokenSource::CacheToken` cannot read `~/.cache/huggingface/token` because that
480 -path is outside the sandbox. `HF_TOKEN` must be set as an environment variable
481 -**when building the Rust static library**:
482 -
483 -```bash
484 -export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
485 -./scripts/build-rust.sh ios
486 -```
487 -
488 -`option_env!("HF_TOKEN")` in `src/inference/token.rs` bakes it into the binary
489 -at compile time. Setting it after the fact has no effect.
490 -
491 ----
492 -
493 -## Architecture
494 -
495 -```
496 -┌──────────────────────────────────────────────────────┐
497 -│ React Native / TypeScript │
498 -│ OndeChatEngine, free functions (index.ts) │
499 -└─────────────────────┬────────────────────────────────┘
500 - │ JSON strings over Expo bridge
501 - ▼
502 -┌──────────────────────────────────────────────────────┐
503 -│ OndeInferenceModule.swift (Expo Module) │
504 -│ - configureApplicationFilesystem() │
505 -│ - validateJsonResult() / validateJsonArray() │
506 -│ - AsyncFunction / Function definitions │
507 -└─────────────────────┬────────────────────────────────┘
508 - │ C FFI via @_silgen_name
509 - ▼
510 -┌──────────────────────────────────────────────────────┐
511 -│ libonde_react_native.a (Rust static lib) │
512 -│ C API: onde_engine_create, onde_engine_load_*, ... │
513 -│ Rust engine: ChatEngine, mistral.rs, Metal/CPU │
514 -└──────────────────────────────────────────────────────┘
515 -```
516 -
517 -The Swift module **does not use UniFFI**. It calls the Rust C API directly via
518 -`@_silgen_name` declarations, communicating via JSON strings over the Expo
519 -Module bridge.
520 -
521 -This is different from the Swift XCFramework SDK (which uses UniFFI-generated
522 -`onde.swift`) and the Kotlin SDK (which uses UniFFI-generated `onde.kt`).
523 -
524 ----
525 -
526 -## Expo Module Definition Reference
527 -
528 -All methods in `OndeInferenceModule.swift` follow this pattern:
529 -
530 -| Expo construct | Use for |
531 -|---|---|
532 -| `Function(name)` | Synchronous calls (isLoaded, setSystemPrompt, etc.) |
533 -| `AsyncFunction(name)` | All calls that invoke Rust (model load, inference, etc.) |
534 -| `OnCreate` | Engine initialisation — run `configureApplicationFilesystem()` here |
535 -| `OnDestroy` | Engine cleanup — call `onde_engine_destroy` |
536 -
537 -Return types:
538 -- Simple sync functions: `Bool`, `Int`
539 -- All complex types: `String` — raw JSON from Rust, **never** `[String: Any]`
540 -
541 ----
542 -
543 -## TypeScript Bridge Contract
544 -
545 -### `OndeInferenceModule.ts` — raw native interface
546 -
547 -All async methods return `Promise<string>` (raw JSON strings from Rust):
548 -
549 -```typescript
550 -export interface OndeInferenceNativeModule {
551 - // Sync
552 - isLoaded(): boolean;
553 - setSystemPrompt(prompt: string): void;
554 - clearHistory(): number;
555 -
556 - // Async — all return JSON strings
557 - loadDefaultModel(systemPrompt: string | null, samplingJson: string | null): Promise<string>;
558 - sendMessage(message: string): Promise<string>;
559 - info(): Promise<string>;
560 - history(): Promise<string>;
561 -
562 - // Sync config — return JSON strings
563 - defaultModelConfig(): string;
564 - defaultSamplingConfig(): string;
565 -}
566 -```
567 -
568 -### `index.ts` — public API layer
569 -
570 -Serialises inputs to JSON, calls native, deserialises JSON responses:
571 -
572 -```typescript
573 -// Input serialisation (camelCase TS → snake_case Rust)
574 -function serializeSampling(config: SamplingConfig): string {
575 - return JSON.stringify({ temperature: config.temperature, top_p: config.topP, ... });
576 -}
577 -
578 -// Output deserialisation (snake_case Rust → camelCase TS)
579 -function parseResult<T>(json: string): T {
580 - const parsed = JSON.parse(json); // JSON.parse — expects a string
581 - if (parsed.error) throw new OndeError(parsed.error);
582 - return parsed as T;
583 -}
584 -```
585 -
586 ----
587 -
588 -## Metro Config for `file:..` SDK Development
589 -
590 -```js
591 -const { getDefaultConfig } = require("expo/metro-config");
592 -const path = require("path");
593 -
594 -const sdkRoot = path.resolve(__dirname, ".."); // ../sdk/react-native/
595 -const config = getDefaultConfig(__dirname);
596 -
597 -// 1. Watch the SDK so Metro picks up changes
598 -config.watchFolders = [sdkRoot];
599 -
600 -// 2. Map the package name to the SDK root
601 -config.resolver.extraNodeModules = {
602 - "@ondeinference/react-native": sdkRoot,
603 -};
604 -
605 -// 3. Allow transitive deps (e.g. @babel/runtime) to resolve from
606 -// the example's node_modules when files outside the example are processed
607 -config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")];
608 -
609 -// 4. Block the SDK's own node_modules to prevent react-native version conflicts
610 -config.resolver.blockList = [
611 - new RegExp(
612 - path.resolve(sdkRoot, "node_modules").replace(/\\/g, "\\\\") + "/.*",
613 - ),
614 -];
615 -
616 -module.exports = config;
617 -```
618 -
619 ----
620 -
621 -## Rust C API Surface
622 -
623 -The Swift module binds to these C functions via `@_silgen_name`:
624 -
625 -| Function | Returns | Notes |
626 -|---|---|---|
627 -| `onde_engine_create` | `UnsafeMutableRawPointer?` | Allocates engine |
628 -| `onde_engine_destroy` | `void` | Frees engine |
629 -| `onde_engine_load_default_model` | `UnsafeMutablePointer<CChar>?` | JSON result string |
630 -| `onde_engine_load_model` | `UnsafeMutablePointer<CChar>?` | JSON result string |
631 -| `onde_engine_unload_model` | `UnsafeMutablePointer<CChar>?` | JSON result string |
632 -| `onde_engine_is_loaded` | `Bool` | Sync |
633 -| `onde_engine_info` | `UnsafeMutablePointer<CChar>?` | JSON result string |
634 -| `onde_engine_send_message` | `UnsafeMutablePointer<CChar>?` | JSON result string |
635 -| `onde_engine_generate` | `UnsafeMutablePointer<CChar>?` | JSON result string |
636 -| `onde_engine_history` | `UnsafeMutablePointer<CChar>?` | JSON array string |
637 -| `onde_engine_clear_history` | `UInt64` | Sync, count |
638 -| `onde_engine_set_system_prompt` | `void` | Sync |
639 -| `onde_engine_set_sampling` | `void` | Sync |
640 -| `onde_engine_push_history` | `void` | Sync |
641 -| `onde_free_string` | `void` | Must call for every non-null CChar* result |
642 -| `onde_default_model_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
643 -| `onde_qwen25_1_5b_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
644 -| `onde_qwen25_3b_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
645 -| `onde_default_sampling_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
646 -| `onde_deterministic_sampling_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
647 -| `onde_mobile_sampling_config` | `UnsafeMutablePointer<CChar>?` | JSON result string |
648 -
649 -**Always call `onde_free_string` after consuming each result pointer.** The
650 -Rust side allocates the string; Swift is responsible for freeing it.
651 -
652 -```swift
653 -private func consumeRustString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
654 - guard let ptr = ptr else { return nil }
655 - let str = String(cString: ptr)
656 - onde_free_string(ptr) // ← must free every non-null pointer
657 - return str
658 -}
659 -```
660 -
661 ----
662 -
663 -## build-rust.sh — iOS Build Flow
664 -
665 -```bash
666 -# 1. Build both targets
667 -cargo build --manifest-path rust/Cargo.toml --target aarch64-apple-ios --release
668 -cargo build --manifest-path rust/Cargo.toml --target aarch64-apple-ios-sim --release
669 -
670 -# 2. Copy to separate dirs (same filename — cannot lipo)
671 -mkdir -p ios/rust/device ios/rust/simulator
672 -cp target/aarch64-apple-ios/release/libonde_react_native.a ios/rust/device/
673 -cp target/aarch64-apple-ios-sim/release/libonde_react_native.a ios/rust/simulator/
674 -
675 -# 3. Create XCFramework (CocoaPods picks right slice automatically)
676 -rm -rf ios/rust/OndeReactNative.xcframework
677 -xcodebuild -create-xcframework \
678 - -library ios/rust/device/libonde_react_native.a \
679 - -library ios/rust/simulator/libonde_react_native.a \
680 - -output ios/rust/OndeReactNative.xcframework
681 -```
682 -
683 -After rebuilding, run `pod install` in `example/ios/` and then
684 -`npx expo run:ios --device` or `npx expo run:ios` (simulator).
685 -
686 ----
687 -
688 -## Running on Physical Device
689 -
690 -```bash
691 -# From example/
692 -npx expo run:ios --device
693 -```
694 -
695 -For a physical device build you need:
696 -- Apple Developer team set in `app.json` → `expo.ios.appleTeamId`
697 -- The App Group `group.com.ondeinference.apps` registered in the developer portal
698 -- The App Group capability enabled on the App ID
699 -- `com.apple.developer.kernel.increased-memory-limit` in entitlements
700 -
701 -**Blank white screen on device** = Metro bundle not loading. Ensure:
702 -1. Metro is running (`npx expo start --clear`)
703 -2. Phone and Mac are on the same network (or use Tunnel)
704 -3. Rebuild after native changes (`npx expo run:ios --device` — not just `expo start`)
705 -
706 ----
707 -
708 -## .gitignore for the SDK
709 -
710 -```gitignore
711 -node_modules/
712 -build/ # tsc output
713 -rust/target/ # Rust build artifacts
714 -ios/build/
715 -ios/rust/*.a
716 -ios/rust/*.xcframework
717 -ios/rust/device/
718 -ios/rust/simulator/
719 -android/build/
720 -android/src/main/jniLibs/
721 -example/.expo/
722 -example/android/
723 -example/ios/build/
724 -example/ios/Pods/
725 -example/ios/.xcode.env.local
726 -.DS_Store
727 -*.xcuserstate
728 -```
729 -
730 ----
731 -
732 -## npm Publishing
733 -
734 -```bash
735 -cd sdk/react-native
736 -npm run build # tsc → build/
737 -npm publish # publishes build/, ios/, android/, scripts/
738 -```
739 -
740 -The `.npmignore` excludes:
741 -- `example/`
742 -- `rust/`
743 -- `node_modules/`
744 -- `src/` (consumers use the compiled `build/`)
745 -
746 -The `package.json` `files` field explicitly includes:
747 -`build/`, `ios/`, `android/`, `scripts/`, `expo-module.config.json`
748 -
749 ----
750 -
751 -## Common Pitfalls Table
752 -
753 -| Pitfall | Symptom | Fix |
754 -|---|---|---|
755 -| Missing `nodeModulesPaths` in metro.config.js | `Cannot resolve @babel/runtime/helpers/asyncToGenerator` | Add `config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")]` |
756 -| Deployment target mismatch | Pod installs but `OndeInference` missing from `Podfile.lock` | Set `"ios.deploymentTarget": "16.0"` in `Podfile.properties.json` AND `app.json` |
757 -| Single `.a` file for device + simulator | Linker error: "building for iOS-simulator but linked for iOS" | Use `xcodebuild -create-xcframework` to bundle both slices |
758 -| Two `.a` files with same name in `vendored_libraries` | CocoaPods "conflicting names" error | Use `vendored_frameworks` with an XCFramework instead |
759 -| Swift returning `[String: Any]` from Expo Module | `JSON parse error: unexpected character: o` | Return `String` from all Expo Module functions; validate but do not deserialise |
760 -| `HF_HOME` not set on iOS | "error 0" or model load fails immediately | Call `configureApplicationFilesystem()` in `OnCreate` and before model load |
761 -| Wrong cache path (`OndeInference/huggingface/hub/`) | Models not shared across Onde apps | Use `<container>/models/hub/` to match Tauri/Swift convention |
762 -| `EngineStatus` lowercase from Rust | Input stays disabled after model loads; status shows as `"ready"` not `"Ready"` | Normalize in `refreshStatus()` with `switch(status.toLowerCase())` |
763 -| `FlatList` swallows taps | TextInput can't be focused | Add `keyboardShouldPersistTaps="handled"` to `FlatList` |
764 -| Model hangs on Simulator | Stuck in loading indefinitely | Test model loading on a physical device; Simulator is unreliable for this |
765 -| Stale `.part`/`.lock` files | Model download never completes on retry | Delete `models--<org>--<model>` from the shared container and retry |
766 -| `HF_TOKEN` not baked in | Auth fails on HuggingFace download; "401 Unauthorized" in Rust logs | Set `HF_TOKEN` env var **before** building the Rust static library |
767 -| `expo prebuild --clean` wipes `Podfile.properties.json` | Deployment target reverts to 15.1; autolinking drops OndeInference | Re-add `"ios.deploymentTarget": "16.0"` to `Podfile.properties.json` after every `--clean` |
768 -| Physical device blank white screen | App launches, nothing renders | Ensure Metro is running and on same network; use `npx expo run:ios --device` |
769 -| Running with `expo start` instead of `expo run:ios` | "Cannot find native module 'OndeInference'" | Custom native modules require a native build — use `expo run:ios` not Expo Go |
\ No newline at end of file