--- created: '2026-05-16' updated: '2026-05-19' tags: - project - dev - ios - swift - android - skip --- # Reflect App — Dev Setup & Architecture > **iOS (reflect-app):** `~/Desktop/reflect-app/` — нативный SwiftUI, не трогать > **Cross-platform (reflect-skip):** `~/Developer/reflect-skip/` — Skip Fuse, iOS + Android > Gitea: `git.mallexxx.duckdns.org/git_admin/reflect-app` --- ## Статус (2026-05-19) ### Что реализовано | Компонент | Статус | |-----------|--------| | iOS build (reflect-app) | ✅ Build succeeded, 30/30 тестов | | Android build (reflect-skip) | ✅ APK собирается, запускается на эмуляторе | | Экран 0: Privacy intro | ✅ Личный текст, GDPR opt-out, дисклеймер | | Онбординг | ✅ 3 info-слайда + 5 вопросов + AI инсайт | | Сессия: dynamic intro | ✅ AI-генерированное вступление + max 2 крючка из истории | | Сессия: one-by-one вопросы | ✅ Вопросы по одному, батч-подбивка агенту | | Сессия: closing phrase | ✅ Тёплая фраза от Narrator'а | | Сессия: one last thing | ✅ Маленький input + неакцентная кнопка Завершить | | Dual-bot pipeline | ✅ Narrator (Haiku) → Analyst (Sonnet) | | Crisis detection | ✅ Hardcoded RU+EN, CrisisView | | Profile vault | ✅ profile.md, sessions/, insights.md | | App Lock (Face ID) | ✅ BiometricAuthentication | | Settings: API key, proxy | ✅ Keychain + LocalProxy | | Settings: Сбросить данные | ✅ Алерт + полный сброс + онбординг заново | | Session scheduler | ✅ UNCalendarNotification | ### Что не реализовано (план) | Компонент | Приоритет | |-----------|-----------| | ProfileView: история сессий | 🟠 Средний | | Session recovery (незавершённые) | 🟠 Средний | | Полноценный sessions/YYYY-MM-DD.md | 🟡 Низкий | | RevenueCat paywall | 🔴 Нужен до публикации | | Голосовой ввод (iOS) | 🟡 Phase 2 | | Push-уведомления opt-in | 🟡 Phase 2 | --- ## Android / Skip Fuse — сборка ### Требования - Xcode 16+, `xcodebuild` - Java 21: `/opt/homebrew/opt/openjdk@21` - Android SDK: `~/Library/Android/sdk` - Android Emulator: `reflect-android` (Pixel 6, API 35) - Skip CLI: `skip` (из Homebrew или из Xcode DerivedData) ### Полная сборка (первый раз или после новых Swift файлов) ```bash cd ~/Developer/reflect-skip export ANDROID_HOME=~/Library/Android/sdk export JAVA_HOME=/opt/homebrew/opt/openjdk@21 export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" # Шаг 1: Xcode генерирует Kotlin стабы (skipstone) + собирает Android xcodebuild \ -workspace Project.xcworkspace \ -scheme "ReflectApp App" \ -destination "generic/platform=iOS Simulator" \ -configuration Debug \ -skipPackagePluginValidation \ SKIP_ACTION=build # Шаг 2: Линкуем symlinks субмодулей (один раз после очистки) # Делается скриптом или вручную — см. раздел ниже # Шаг 3: Gradle собирает APK cd Darwin skip gradle -p ../Android assembleDebug ``` ### Быстрая пересборка (инкрементальная) ```bash cd ~/Developer/reflect-skip/Darwin export ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21 export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" skip gradle -p ../Android assembleDebug ``` ### Деплой на эмулятор ```bash export ANDROID_HOME=~/Library/Android/sdk export PATH="$ANDROID_HOME/platform-tools:$PATH" # Запустить эмулятор если не запущен $ANDROID_HOME/emulator/emulator -avd reflect-android & APK=~/Developer/reflect-skip/.build/Android/app/outputs/apk/debug/app-debug.apk adb -s emulator-5554 install -r "$APK" adb -s emulator-5554 shell am start -n com.reflect.app/reflect.app.MainActivity ``` --- ## ⚠️ Критические особенности Skip Fuse ### 1. Новые Swift View файлы требуют ручного Kotlin стаба При добавлении нового `*.swift` файла с View в `Sources/ReflectApp/Views/`: skipstone **не всегда** автоматически генерирует `.kt` стаб. Нужно создать вручную. **Шаблон** (копируем `OnboardingView.kt`, меняем класс): ```kotlin package reflect.app import skip.lib.* import skip.ui.* import skip.foundation.* import skip.model.* @androidx.annotation.Keep internal class MyNewView: skip.ui.View, skip.bridge.SwiftPeerBridged, skip.lib.SwiftProjecting { var Swift_peer: skip.bridge.SwiftObjectPointer = skip.bridge.SwiftObjectNil constructor(Swift_peer: skip.bridge.SwiftObjectPointer, marker: skip.bridge.SwiftPeerMarker?) { this.Swift_peer = Swift_peer } fun finalize() { Swift_release(Swift_peer); Swift_peer = skip.bridge.SwiftObjectNil } private external fun Swift_release(Swift_peer: skip.bridge.SwiftObjectPointer) override fun Swift_peer(): skip.bridge.SwiftObjectPointer = Swift_peer override fun equals(other: Any?): Boolean { if (other !is skip.bridge.SwiftPeerBridged) return false return Swift_peer == other.Swift_peer() } override fun hashCode(): Int = Swift_peer.hashCode() override fun body(): skip.ui.View { return skip.ui.ComposeBuilder { ctx -> Swift_composableBody(Swift_peer)?.Compose(ctx) ?: skip.ui.ComposeResult.ok } } private external fun Swift_composableBody(Swift_peer: skip.bridge.SwiftObjectPointer): skip.ui.View? constructor(/* params */) { Swift_peer = Swift_constructor_0(/* params */) } private external fun Swift_constructor_0(/* params */): skip.bridge.SwiftObjectPointer override fun Swift_projection(options: Int): () -> Any = Swift_projectionImpl(options) private external fun Swift_projectionImpl(options: Int): () -> Any @androidx.annotation.Keep companion object {} } ``` **Файл кладём в оба места:** ``` ~/Library/Developer/Xcode/DerivedData/Project-bpfpf.../Build/Intermediates.noindex/BuildToolPluginIntermediates/reflect-skip.output/ReflectApp/skipstone/ReflectApp/src/main/kotlin/reflect/app/ ~/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone/ReflectApp/src/main/kotlin/reflect/app/ ``` ### 2. Kotlin субмодули — symlinks После очистки кеша/DerivedData Kotlin исходники субмодулей (SkipLib, SkipBridge, etc.) перестают быть видны Gradle. Нужно перелинковать: ```bash DERIVED="$HOME/Library/Developer/Xcode/DerivedData/Project-bpfpf..." SKIPSTONE="$HOME/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone" for MODULE in SkipLib SkipBridge SkipFoundation SkipUI SkipAndroidBridge SkipModel SkipSwiftUI SkipUnit; do case "$MODULE" in SkipLib) PKG="skip-lib" ;; SkipBridge) PKG="skip-bridge" ;; SkipFoundation) PKG="skip-foundation" ;; SkipUI) PKG="skip-ui" ;; SkipAndroidBridge) PKG="skip-android-bridge" ;; SkipModel) PKG="skip-model" ;; SkipSwiftUI) PKG="skip-fuse-ui" ;; SkipUnit) PKG="skip-unit" ;; esac REAL_KT="$DERIVED/Build/Intermediates.noindex/BuildToolPluginIntermediates/${PKG}.output/${MODULE}/skipstone/${MODULE}/src/main/kotlin" mv "$SKIPSTONE/$MODULE/src/main/kotlin" "$SKIPSTONE/$MODULE/src/main/kotlin.bak" 2>/dev/null || true ln -sfn "$REAL_KT" "$SKIPSTONE/$MODULE/src/main/kotlin" done ``` ### 3. AndroidBridge.initBridge — обязательно до первого JNI вызова В `Android/app/src/main/kotlin/Main.kt` порядок в `onCreate`: ```kotlin ProcessInfo.launch(applicationContext) AndroidBridge.initBridge("ReflectApp") // ← ДО AppDelegate.shared.onInit() AppDelegate.shared.onInit() ``` --- ## Структура проекта (reflect-skip) ``` reflect-skip/ ├── Package.swift ← SPM, SkipFuseUI зависимость ├── Skip.env ← ANDROID_PACKAGE_NAME=reflect.app ├── Project.xcworkspace/ ← Workspace для xcodebuild ├── Darwin/ ← Xcode проект (iOS таргет + Skip build phases) │ ├── ReflectApp.xcodeproj/ │ └── Sources/Main.swift ├── Android/ ← Gradle Android проект │ ├── app/src/main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/Main.kt ← Entry point, AndroidBridge.initBridge │ └── settings.gradle.kts └── Sources/ReflectApp/ ├── ReflectAppMain.swift ← App root + PrivacyIntroView → Onboarding → Home ├── Models/ ├── Services/ │ ├── ClaudeService.swift ← Claude API (cloud + local proxy) │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы + tools) │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe) │ ├── CrisisDetector.swift ← Hardcoded SI/SH │ ├── ProfileManager.swift ← On-device vault (profile.md, sessions/) │ ├── SessionManager.swift ← Оркестратор: intro/closing/pipeline │ └── ScheduleNegotiationService.swift └── Views/ ├── PrivacyIntroView.swift ← Экран 0: личный текст + GDPR ├── OnboardingView.swift ← Онбординг ├── HomeView.swift ├── SessionView.swift ← Фазы: loading/intro/dialogue/closing/oneLastThing ├── CardView.swift ├── SettingsView.swift ← AI backend + Сбросить данные └── OrbCharacter.swift ``` --- ## Архитектура флоу онбординга ``` Первый запуск → PrivacyIntroView (privacy_accepted) → OnboardingView (5 вопросов + AI инсайт → profile.md) → HomeView → SessionView (loading → intro → dialogue → closing → oneLastThing → done) ``` --- ## On-device хранилище ``` Documents/UserProfile/ ├── profile.md ← паттерны, наблюдения, счётчик сессий ├── insights.md ← кристаллизованные инсайты └── sessions/ ├── index.md ← таблица дат/тем для быстрого поиска └── 2026-05-19.md ``` **Сброс:** Settings → Данные → Сбросить все данные → удаляет `UserProfile/`, сбрасывает `privacy_accepted`, `onboarding_completed`, API key. --- ## Локальный AI прокси ```bash cd ~/Developer/reflect-app # iOS проект python3 proxy_server.py --backend claude-code # OAuth через Hermes # или python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b ``` В Settings приложения: AI Backend → Локальный сервер → URL сервера. --- ## Архитектура двойного бота ``` User input (1-е лицо) ↓ CrisisDetector.detect() — hardcoded, <5ms ↓ (если .critical → CrisisView) NarratorBot (Haiku) — переформулировка в 3-е лицо ↓ AnalystBot (Sonnet) — читает profile.md + инструменты (search_sessions, read_profile...) ↓ 1 вопрос из батча → ответ → следующий вопрос → нумерованная подбивка → AnalystBot ``` **Закрытие сессии:** 1. `endSession()` → AnalystBot генерирует наблюдение (3-е лицо) 2. NarratorBot генерирует тёплую closing phrase 3. One last thing: маленький input 4. Если нет расписания → предложить дату следующей сессии