--- title: "Skip.tools PoC — оценка зрелости" aliases: - "Skip PoC" - "Skip Tools evaluation" - "skip-tools-poc" - "Skip.tools" created: '2026-05-16' updated: '2026-05-31' tags: - project - app - psychology - dev - skip - android related: - "[[personal/projects/psychologist-app/overview]]" - "[[personal/projects/psychologist-app/dev-setup]]" --- # Skip.tools: Оценка зрелости для AI-приложения с анимированным персонажем и голосом > **Дата исследования:** 16 мая 2026 > **Версия Skip:** 1.8.14 (актуальная) > **Контекст:** AI-приложение рефлексии на SwiftUI с анимированным blob/orb персонажем (Rive) и голосовым вводом/выводом (OpenAI Realtime API или Whisper+TTS) --- ## Вердикт TL;DR > **⚠️ УСЛОВНО ГОТОВ — с существенными оговорками** > > Skip в 2026 году — это уже не эксперимент. Фреймворк активен, свободен, компилирует настоящий Jetpack Compose. Но для нашего конкретного кейса есть два критических барьера: **Rive не поддерживается официально** и требует платформо-специфичного workaround, а **WebSocket был сломан** до Skip 1.8/Swift 6.3 (сейчас починен). Рекомендация: **iOS-first MVP + Skip на неделю PoC**, принять решение по результатам. --- ## 1. Текущий статус Skip.tools (2025–2026) ### Жив ли проект? **Однозначно да.** Метрики по состоянию на май 2026: | Метрика | Значение | |---------|---------| | GitHub Stars | ~3,000 ⭐ | | Forks | 101 | | Последний релиз | v1.8.14 (8 мая 2026) | | Лицензия | MPL-2.0 (полностью open source с v1.7) | | Язык | Swift 100% | | Режимы работы | Skip Fuse (нативный) / Skip Lite (транспайл) | Важный поворот: в **феврале 2025** Skip стал соучредителем [Swift on Android Working Group](https://forums.swift.org/t/swift-on-android-working-group/77780), а **Swift 6.3** официально шипнул Swift SDK для Android. Skip 1.8 использует этот официальный SDK — больше нет зависимости от кастомного форка. ### Два режима: что выбрать? | | **Skip Fuse** (нативный) | **Skip Lite** (транспайл) | |---|---|---| | Механизм | Swift компилируется нативно для Android | Swift → Kotlin (исходный код) | | Плюсы | Полный Swift, все сторонние пакеты, детерминированная память | Прямой доступ к Kotlin/Java API, читаемый Kotlin-вывод, меньший размер | | Минусы | +60 МБ к APK, медленная сборка | Не весь Swift транслируется, меньше пакетов | | Рекомендация для нас | ✅ **Лучший выбор** — полный Swift, rich animations | — | ### Что поддерживается из SwiftUI? Уровень покрытия (из официальной матрицы): **Полная поддержка (✅):** - `@State`, `@Binding`, `@Environment`, `@EnvironmentObject`, `@ObservedObject`, `@StateObject`, `@AppStorage` - `AsyncImage`, `Button`, `HStack`, `VStack`, `ZStack`, `ScrollView`, `List`, `Form` - `LinearGradient`, `RadialGradient`, `Circle`, `Rectangle`, `RoundedRectangle`, `Capsule` - `NavigationStack`, `TabView`, `Sheet`, `Alert` - `Text`, `TextField`, `SecureField`, `Toggle`, `Slider`, `Picker` - `withAnimation` (есть известный баг — см. ниже) **Частичная поддержка / проблемы:** - `Canvas` — **статус неясен** (не упоминается в матрице явно, вероятно не поддерживается) - `TimelineView` — аналогично, не подтверждён - `@GestureState` — только Skip Fuse - `AppStorage` с optional values — не работает **Известные баги (актуальные issues):** | Issue | Проблема | Дата | |-------|----------|------| | [#402](https://github.com/skiptools/skip-ui/issues/402) | `withAnimation()` утекает в глобальное состояние на Android | Апр 2026 | | [#687](https://github.com/skiptools/skip/issues/687) | State не обновляется под NavigationLink (Fuse) | Май 2026 | | [#376](https://github.com/skiptools/skip-ui/issues/376) | NavigationLink throttling (задержка навигации) | Мар 2026 | | [#339](https://github.com/skiptools/skip-ui/issues/339) | `.frame(maxWidth:)` игнорирует границы родителя | Фев 2026 | | [#331](https://github.com/skiptools/skip-ui/issues/331) | Text с `%` и Markdown рендерится неправильно | Фев 2026 | > **Критически важно для нас:** баг с `withAnimation()` (#402) напрямую влияет на анимированный персонаж. ### Приложения в production Официально подтверждённые: - **Skip Showcase** — каталог SwiftUI компонентов, опубликован в App Store и Google Play - **Skip Bookings** — тревел-букинг с картами (пример) - **Skip Weather** — погода с async networking - В v2.3.6 Showcase добавили целую игру Block Blast на ~1000 строк чистого Swift > Третьесторонних production-приложений в публичном доступе не найдено — Skip пока используется преимущественно инди-разработчиками. --- ## 2. Rive + Skip: совместимость ### Официальная поддержка **❌ Нет официального пакета `skip-rive`.** Список официальных Skip-модулей: ``` skip-motion (Lottie), skip-av (AVFoundation), skip-firebase, skip-revenue (RevenueCat)... ``` Rive в нём отсутствует. ### Как Rive работает на iOS Rive iOS runtime (`rive-app/rive-ios`, 780 ⭐, v6.20.3, MIT License) использует: - **Objective-C++ (~32% кода)** для C++ рантайма Rive - Интеграция в SwiftUI через `RiveViewModel` + `RiveView` ```swift // Типичный код на iOS import RiveRuntime struct CharacterView: View { @StateObject var riveVM = RiveViewModel(fileName: "blob_character") var body: some View { riveVM.view() .frame(width: 300, height: 300) } } ``` **Проблема для Skip:** Rive iOS package содержит нативный C++ runtime и Objective-C++ bindings. Skip **не может транспилировать Objective-C++ код** — это за пределами его возможностей. ### Workaround: Platform-specific через ComposeView Skip предоставляет мощный механизм для именно такого случая: ```swift struct CharacterView: View { let characterState: CharacterState var body: some View { #if os(Android) // Android: используем Rive Android SDK через Compose ComposeView { RiveAndroidComposer(state: characterState) } #else // iOS: нативный RiveRuntime RiveNativeView(state: characterState) #endif } } // iOS-only код (полный Swift, без ограничений) #if !SKIP import RiveRuntime struct RiveNativeView: View { let state: CharacterState @StateObject var riveVM = RiveViewModel(fileName: "blob_character") var body: some View { riveVM.view() .onAppear { riveVM.triggerInput("setState", value: state.rawValue) } } } #endif // Android Kotlin через #if SKIP блок #if SKIP import app.rive.runtime.kotlin.compose.RiveAnimation struct RiveAndroidComposer: ContentComposer { let state: CharacterState @Composable func Compose(context: ComposeContext) { RiveAnimation( resId = R.raw.blob_character, modifier = context.modifier, stateMachineName = "State Machine", onInit = { controller in controller.setBooleanState("State Machine", "isListening", state == .listening) } ) } } #endif ``` **Оценка workaround:** - ✅ Технически реализуемо - ✅ iOS остаётся чистым нативным RiveRuntime - ⚠️ Нужно поддерживать две реализации - ⚠️ Требует добавить зависимость `rive-android` в Gradle вручную через `skip.yml` - ⏱️ Оценка трудозатрат: 2-3 дня дополнительно ### Lottie как альтернатива Rive **`skip-motion` (официальный пакет)** — Lottie поддерживается! ```swift import SkipMotion struct CharacterView: View { var body: some View { MotionView(lottie: lottieAnimationData) .frame(width: 300, height: 300) } } ``` | Параметр | Статус | |----------|--------| | Версия | v0.7.2 (Jan 2026) | | iOS backend | lottie-ios v4.6.0 | | Android backend | lottie-android v6.7.1 | | Размер | +~1 МБ к ipa/apk | | Управление воспроизведением | ⚠️ Раннее состояние, ограниченные контролы | | State machine | ❌ Нет (в отличие от Rive) | **Ключевое ограничение Lottie:** у него нет **State Machine** как у Rive. Для персонажа с динамическими состояниями (listening, thinking, speaking) это критично — нужно либо делать несколько анимаций, либо переключать между файлами. В Rive это нативно. ### SwiftUI-native анимированный персонаж (без Rive/Lottie) Возможный подход: написать blob/orb через SwiftUI shapes + `withAnimation`: ```swift struct BlobCharacterView: View { @State private var animationPhase: Double = 0 var body: some View { TimelineView(.animation) { timeline in Canvas { context, size in // Custom blob morphing via Bezier paths let path = blobPath(for: timeline.date.timeIntervalSince1970) context.fill(path, with: .color(.purple.opacity(0.8))) } } .frame(width: 200, height: 200) } } ``` **Проблема:** `Canvas` и `TimelineView` — **не подтверждены** в матрице совместимости Skip. Вероятно, не поддерживаются на Android (не найдены в списке поддерживаемых Views). Плюс баг `withAnimation()` (#402). > **Вывод по анимации:** Rive через workaround — самый мощный вариант, но требует дополнительных усилий. Lottie безопаснее для Skip, но функционально беднее. Pure SwiftUI blob — рискованно из-за Canvas/TimelineView. --- ## 3. Голосовая интеграция + Skip ### skip-av: статус AVFoundation Официальный пакет `skip-av` (v0.7.1, 15 мая 2026, 12 ⭐): | API | Android-поддержка | Что работает | |-----|-------------------|--------------| | `AVAudioRecorder` | 🟢 **High** | `record()`, `stop()`, `pause()`, `isRecording`, `peakPower`, `averagePower` | | `AVAudioPlayer` | 🟢 **High** | `play()`, `pause()`, `stop()`, `volume`, `rate`, `duration` | | `AVPlayer` | 🟠 Low | Только базовый init/play/pause | | `VideoPlayer` | 🟡 Medium | `init(player:)` | **Бэкенд на Android:** `MediaRecorder` + `MediaPlayer` (не ExoPlayer для записи). ```swift // Запись голоса — работает на обеих платформах через skip-av let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 16000, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // Platform-specific путь к файлу let recordingURL: URL = { #if SKIP let context = ProcessInfo.processInfo.androidContext let file = java.io.File(context.filesDir, "voice_input.m4a") return URL(fileURLWithPath: file.absolutePath) #else return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) .first!.appendingPathComponent("voice_input.m4a") #endif }() audioRecorder = try AVAudioRecorder(url: recordingURL, settings: settings) audioRecorder?.record() ``` Нужно добавить в AndroidManifest.xml: ```xml ``` ### WebSocket для OpenAI Realtime API **История баг-фикса:** | Событие | Дата | |---------|------| | Проблема #559 открыта: libcurl без WebSocket | Дек 2025 | | Фикс смерджен в swift-docker | 23 Дек 2025 | | Swift 6.3 официально релизнут с WebSocket | Мар 2026 | | Skip 1.8 обновлён на официальный Swift 6.3 SDK | Май 2026 | **Текущий статус (Skip 1.8.14):** WebSocket **должен работать** — Skip 1.8 использует Swift 6.3, где проблема решена. Но статус issue #559 остаётся "Open" — рекомендуется проверить в PoC. ```bash # Проверить активный SDK skip android sdk list # Должен показать swift-6.3-..._android skip android build --version ``` **Альтернативный подход (более безопасный):** Whisper + TTS вместо Realtime API ```swift // Более Skip-friendly архитектура: // 1. Запись голоса → AVAudioRecorder (skip-av) // 2. Отправка файла на Whisper API → URLSession (HTTP POST, не WebSocket) // 3. Ответ через GPT-4 → URLSession // 4. TTS → URLSession, воспроизведение → AVAudioPlayer (skip-av) ``` HTTP через URLSession **полностью поддерживается** Skip — нет никаких проблем. Whisper+TTS архитектура безопаснее для MVP. ### WebRTC (skip-livekit) Интересно: в официальных модулях есть `skip-livekit` — WebRTC через LiveKit. Но для OpenAI Realtime API WebRTC нужен другой конкретный эфемерный ключ, не LiveKit. Это не подходит напрямую. ### Механизм platform-specific кода Skip предоставляет три директивы препроцессора: ```swift // Код только для Android (в Fuse: нативный Swift; в Lite: транспилируется) #if os(Android) // ... #endif // Код транспилируется в Kotlin (можно вызывать Kotlin/Java API напрямую) #if SKIP import android.content.pm.PackageManager let hasPermission = context.checkSelfPermission( android.Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED #endif // Только iOS #if !os(Android) import AVFoundation AVAudioSession.sharedInstance().requestRecordPermission { granted in ... } #endif ``` **Это мощный escape hatch** — любая iOS-only функциональность доступна без ограничений в `#if !SKIP` блоках. --- ## 4. Альтернативы если Skip не готов ### iOS-first MVP без Android **Плюсы:** - ✅ Нет компромиссов — полный RiveRuntime, полный AVFoundation, WebRTC - ✅ iOS = 5× выше ARPU ($140 vs $69 в год на пользователя) - ✅ Скорость разработки — нет overhead на кросс-платформу - ✅ App Store первичен для платных подписок **Минусы:** - ❌ Упускаем ~3.7B Android пользователей - ❌ Если позже добавлять Android — переписывать **Коммерческая оценка:** Для подписочного приложения iOS-first — **стандартная практика**. Большинство AI-приложений (Calm, Headspace, Replika) стартовали с iOS. Android добавляли позже. ### React Native 2025 | Критерий | Оценка | |----------|--------| | Голос (микрофон, TTS) | ✅ expo-av, хорошая поддержка | | Rive анимация | ✅ `rive-react-native` — официальный runtime! | | OpenAI Realtime API | ✅ нет проблем с WebSocket | | Подписки | ✅ react-native-purchases (RevenueCat) | | Производительность анимаций | ⚠️ JS bridge может давать jank | | Язык для iOS-разработчика | ❌ TypeScript/JS — новый стек | | New Architecture migration | ⚠️ Legacy arch заморожена с июня 2025 | **Итог:** Функционально React Native покрывает все наши нужды. Но требует знания JS/TS экосистемы. Rive Runtime для React Native — официальный и зрелый. ### Flutter 2025 | Критерий | Оценка | |----------|--------| | Голос | ✅ `flutter_sound`, `speech_to_text` | | Rive анимация | ✅ официальный `rive` пакет на pub.dev | | OpenAI Realtime API | ✅ WebSocket нативный | | Подписки | ✅ `purchases_flutter` (RevenueCat) | | Производительность | ⚠️ +40 МБ overhead, GC паузы | | iOS 26 "Liquid Glass" UI | ❌ Flutter команда **отказалась** адаптировать Cupertino widgets | | Язык | ❌ Dart — новый стек | **Критическое замечание:** GitHub issues flutter #170310 — Flutter команда подтвердила, что не будет имплементировать новый Apple '26 UI дизайн в Cupertino библиотеке в ближайшее время. Для AI-приложения это может не критично, но iOS UI выглядит устаревшим. ### Kotlin Multiplatform + Compose Multiplatform | Критерий | Оценка | |----------|--------| | UI фреймворк | Compose Multiplatform (нативный Android, mimicked iOS) | | Rive | ✅ `Rive-CMP` (community, v0.4.0, 105 ⭐) | | Голос | ✅ kotlin-native доступ к platform APIs | | Для Swift-разработчика | ❌ Нужен Kotlin — совсем новый стек | | iOS UI | ⚠️ Не SwiftUI — кастомный рендеринг Skia | Для Swift-разработчика KMP — самый далёкий вариант. Хотя технически сильный. ### Capacitor/Ionic **Не рекомендуется** для нашего кейса. WebView-рендеринг, анимации через CSS/JS будут уступать нативным, особенно для персонажа и голоса. --- ## 5. Конкретный план PoC на 1 неделю ### Цель PoC Ответить на три вопроса: 1. Работает ли WebSocket / Whisper API через Skip на Android? 2. Реализуем ли анимированный персонаж (Lottie или Rive workaround)? 3. Работает ли запись голоса через skip-av? ### День 1: Базовый проект ```bash brew tap skiptools/skip && brew install skip skip checkup # Создаём проект в режиме Fuse skip android sdk install # убедиться что 6.3+ skip init --fuse-app --appid=com.yourapp.reflection reflection-app ReflectionApp # Проверяем SDK версию skip android sdk list ``` **Критерий успеха:** Базовое SwiftUI приложение запускается параллельно на iOS Simulator и Android Emulator. ### День 2-3: Голос + сеть ```swift // Тест 1: Запись через AVAudioRecorder // Тест 2: HTTP POST на Whisper API (URLSession) // Тест 3: WebSocket соединение (если нужен Realtime API) ``` ```swift // Проверка WebSocket import Foundation class WebSocketTest: ObservableObject { var task: URLSessionWebSocketTask? func test() { let url = URL(string: "wss://echo.websocket.org")! task = URLSession.shared.webSocketTask(with: url) task?.resume() task?.send(.string("Hello")) { error in print(error == nil ? "✅ WebSocket работает!" : "❌ \(error!)") } } } ``` **Критерий успеха:** Запись работает на Android, HTTP к OpenAI проходит, WebSocket не выдаёт ошибку libcurl. ### День 4-5: Анимированный персонаж **Вариант A (рекомендуется для PoC): Lottie через skip-motion** ```swift // Package.swift: добавить skip-motion .package(url: "https://source.skip.dev/skip-motion.git", "0.0.0"..<"2.0.0") ``` ```swift import SkipMotion struct CharacterView: View { var isListening: Bool var body: some View { MotionView(lottie: isListening ? listeningAnimation : idleAnimation) .frame(width: 250, height: 250) } } ``` **Вариант B (для Rive):** Реализовать RiveCharacterView с #if SKIP/iOS разделением (как показано выше). Добавить rive-android в `skip.yml`: ```yaml # Android/app/src/main/skip.yml dependencies: - app.rive:rive-android:9.12.0 ``` **Критерий успеха:** Персонаж анимируется на обеих платформах, состояния переключаются. ### День 6-7: Интеграция и оценка - Собрать мини end-to-end: нажал кнопку → запись → Whisper → ответ → TTS → воспроизведение → персонаж реагирует - Замерить производительность анимации на Android (fps) - Оценить размер APK ### Критерии успеха/провала PoC | Критерий | Success | Fail | |----------|---------|------| | Голосовая запись | Работает на Android API 28+ | Крэшит или нет звука | | HTTP к OpenAI | Запросы проходят | SSL/network ошибки | | WebSocket | Соединение устанавливается | libcurl ошибка | | Анимация (Lottie) | Плавная, состояния переключаются | Дёргается, не отображается | | Rive workaround (опц.) | Загружается .riv файл | Compile error, не интегрируется | | Общая стабильность | Нет крэшей при 10-мин сессии | Крэши, ANR на Android | ### Fallback план если PoC провалился **Уровень 1 (мягкий фейл — одна проблема):** - WebSocket не работает → используем Whisper+TTS по HTTP - Rive не интегрируется → используем Lottie или SwiftUI анимации - Lottie не устраивает → делаем чистую iOS-версию с Rive, Android откладываем **Уровень 2 (полный фейл Skip):** Переходим на **iOS-first MVP** (натив SwiftUI): - 4-6 недель до первой версии - Полный Rive, полный AVFoundation, OpenAI Realtime API - Android: через 3-6 месяцев либо через React Native (Rive официально поддерживается), либо через Skip если к тому времени созреет --- ## 6. Итоговая рекомендация ### Рекомендованная стратегия: "Skip с Lottie + Whisper-first" ``` Неделя 1-2: PoC на Skip (план выше) Неделя 3-6: MVP на Skip если PoC ✅ ↓ если PoC ❌ iOS-first MVP (нативный SwiftUI + Rive) ``` **Рекомендованный стек для MVP на Skip:** | Компонент | Решение | |-----------|---------| | UI фреймворк | SwiftUI + Skip Fuse | | Анимация персонажа | **Lottie через skip-motion** (безопасно) или Rive с platform-specific workaround | | Голосовой ввод | AVAudioRecorder через **skip-av** | | STT | **Whisper API по HTTP** (не WebSocket) — максимально безопасно | | TTS | OpenAI TTS API по HTTP + AVAudioPlayer | | AI диалог | GPT-4o через HTTP/URLSession | | Подписки | **skip-revenue** (RevenueCat официально поддержан) | | Аналитика | **skip-posthog** (официально) | | Auth | **skip-firebase** (Firebase Auth) | **Почему Whisper+TTS вместо Realtime API:** - WebSocket технически починен в Skip 1.8 (Swift 6.3), но нужно верифицировать в PoC - Whisper+TTS архитектура — надёжнее и дешевле для рефлексии (нет потоковой передачи) - Можно добавить Realtime API позже как premium-фичу **Почему Lottie вместо Rive:** - Официальный skip-motion пакет, нет platform-specific кода - Для MVP не нужна полная State Machine Rive - Rive можно добавить после MVP когда сделаете полноценный workaround ### Ограничения и риски при использовании Skip | Риск | Уровень | Митигация | |------|---------|-----------| | Баг `withAnimation()` (#402) утечка в глобальное состояние | 🔴 Высокий | Тестировать в PoC, использовать `.animation()` modifier вместо `withAnimation` | | Canvas/TimelineView не поддерживаются | 🔴 Высокий | Использовать Lottie/Rive, не делать чистый SwiftUI blob | | WebSocket не верифицирован в production | 🟡 Средний | Whisper+TTS как план A | | Rive workaround требует два кодабейса | 🟡 Средний | Lottie для MVP, Rive потом | | +60 МБ к размеру APK (Skip Fuse) | 🟢 Низкий | Приемлемо для B2C приложения | | Мало примеров third-party production apps | 🟡 Средний | Активное сообщество, официальные примеры | ### Если НЕ использовать Skip прямо сейчас **Лучшая альтернатива для этого конкретного кейса:** 1. **iOS MVP (нативный SwiftUI)** — 4-6 недель, полный Rive, AVFoundation, Realtime API 2. **Android через 6+ месяцев:** - Re-evaluate Skip (к тому времени Canvas поддержка появится) - Или React Native (Rive official runtime, mature ecosystem) **Не рекомендуется:** - Flutter — из-за iOS 26 UI regression и неопределённости команды - Ionic/Capacitor — для анимированного персонажа и голоса WebView недостаточен --- ## Приложения: Ссылки и источники ### Skip официальная документация - [skip.tools/docs](https://skip.tools/docs) — главная документация - [skip.tools/docs/modules/skip-ui/](https://skip.tools/docs/modules/skip-ui/) — матрица совместимости SwiftUI - [skip.tools/docs/modules/skip-av/](https://skip.tools/docs/modules/skip-av/) — AVFoundation поддержка - [skip.tools/docs/platformcustomization/](https://skip.tools/docs/platformcustomization/) — #if SKIP механизм - [skip.tools/compare/](https://skip.tools/compare/) — сравнение с Flutter/React Native ### GitHub Issues (ключевые) - [#559](https://github.com/skiptools/skip/issues/559) — WebSocket не работал в libcurl (починен в 6.3) - [#402](https://github.com/skiptools/skip-ui/issues/402) — withAnimation() глобальный state баг - [#687](https://github.com/skiptools/skip/issues/687) — NavigationLink state bug (Fuse) ### Skip-пакеты для нашего кейса - [skip-motion](https://github.com/skiptools/skip-motion) — Lottie анимации - [skip-av](https://github.com/skiptools/skip-av) — AVFoundation / аудио - [skip-revenue](https://github.com/skiptools/skip-revenue) — RevenueCat подписки - [skip-firebase](https://github.com/skiptools/skip-firebase) — Firebase Auth/Firestore ### Rive - [rive-app/rive-ios](https://github.com/rive-app/rive-ios) — iOS runtime (v6.20.3, 780 ⭐) - [rive-app/rive-android](https://github.com/rive-app/rive-android) — Android runtime (Compose API beta) - [Rive-CMP](https://github.com/muazkadan/Rive-CMP) — Community Compose Multiplatform wrapper ### OpenAI Realtime API - [swift-realtime-openai](https://github.com/m1guelpf/swift-realtime-openai) — Swift SDK (431 ⭐, поддержка WebRTC+WebSocket) --- *Документ создан: 16 мая 2026. Актуален для Skip v1.8.14 и Swift 6.3.* --- ## Связанные заметки - [[personal/projects/psychologist-app/overview|Overview]] - [[personal/projects/psychologist-app/dev-setup|Dev Setup]]