--- created: '2026-05-25' updated: '2026-05-25' tags: - project - psychologist-app - task status: ready --- # Задача: совместимость импорта с форматом Obsidian vault **Цель:** пользователь должен выбрать папку `personal/psychology/` из Obsidian и приложение подхватит профиль и все сессии без конвертации. **Репо:** `~/Developer/reflect-skip` **Затрагиваемые файлы:** - `Sources/ReflectApp/Services/DocumentImportService.swift` - `Sources/ReflectApp/Services/ProfileManager.swift` --- ## Контекст: что сейчас есть в vault ``` personal/psychology/ ├── profile.md ← есть **Имя:** Алексей (добавлено), YAML frontmatter, rich markdown ├── insights.md ← есть, формат: ## Отцовство / ## ADHD с нарративными блоками ├── prompts.md ← не нужен для импорта └── observations/ ← папка с еженедельными файлами ├── 2026-W20.md └── 2026-W21.md ``` **Что ожидает приложение сейчас:** ``` UserProfile/ ├── profile.md ← строка "**Имя:** Алексей" (теперь есть ✅) ├── insights.md ← есть ✅ └── sessions/ ← ДРУГОЕ имя папки ❌ └── YYYY-MM-DD.md ← ДРУГОЙ формат имени файла (еженедельные vs ежедневные) ``` --- ## Изменения в DocumentImportService.swift ### 1. Поддержка папки `observations/` как алиаса `sessions/` В методах `validate()`, `countSessions()`, `apply()` — проверять обе папки. **`countSessions(in:)`** — текущий код: ```swift let sessionsURL = url.appendingPathComponent("sessions") ``` Заменить на: ```swift let sessionsURL = sessionsFolder(in: url) ``` Добавить приватный метод: ```swift private func sessionsFolder(in url: URL) -> URL { let sessions = url.appendingPathComponent("sessions") let observations = url.appendingPathComponent("observations") if FileManager.default.fileExists(atPath: observations.path) { return observations } return sessions } ``` ### 2. Обновить `validate()` — убрать упоминание "sessions" из ошибки Текущий error description (`noProfileFound`) упоминает "папку sessions" — заменить на нейтральное: ``` В выбранной папке нет файла profile.md. Убедитесь что выбрана папка с данными профиля. ``` ### 3. Обновить `apply()` — копировать observations/ → sessions/ В методе `apply()` строка: ```swift let sessionsSrc = result.sourceURL.appendingPathComponent("sessions") ``` Заменить на: ```swift let sessionsSrc = sessionsFolder(in: result.sourceURL) ``` Папка всегда копируется в `sessions/` в app Documents (это правильно — внутреннее имя не меняем). --- ## Изменения в ProfileManager.swift ### 4. Fallback для `sessionCount` при импортированном vault Текущий код ищет regex `- Сессий проведено: N` — vault profile этого не имеет. Результат: `sessionCount` возвращает 0, приложение запускает "первую сессию" даже при 2+ импортированных. Изменить `sessionCount` — добавить fallback: ```swift var sessionCount: Int { // Сначала пробуем разобрать из текста профиля if let content = try? String(contentsOf: profileFileURL, encoding: .utf8), let range = content.range(of: #"- Сессий проведено: (\d+)"#, options: .regularExpression), let numRange = String(content[range]).range(of: #"\d+"#, options: .regularExpression), let count = Int(String(content[range])[numRange]) { return count } // Fallback: считаем фактические файлы в sessions/ let files = (try? FileManager.default.contentsOfDirectory( at: sessionsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles )) ?? [] return files.filter { $0.pathExtension == "md" && $0.lastPathComponent != "index.md" }.count } ``` --- ## Что НЕ нужно менять - Формат файлов сессий (`YYYY-MM-DD.md` vs `2026-W20.md`): BM25 поиск и `loadRecentSessions()` сортируют лексикографически — оба формата работают одинаково хорошо. - Структура profile.md: `loadProfile()` отдаёт raw markdown в Claude — богатый vault-формат работает лучше чем простой app-формат. - insights.md: `loadInsights()` тоже отдаёт raw markdown — vault-формат с секциями лучше flat-списка. - `appendSession()` / `updateProfile()` / `addPattern()`: создают/дополняют нужные секции — при отсутствии секции код добавляет в конец файла. OK. --- ## Acceptance criteria 1. Пользователь выбирает папку `~/obsidian/personal/psychology/` на iOS 2. Validation проходит: имя "Алексей", счётчик сессий = 2 3. После import в `Documents/UserProfile/`: - `profile.md` скопирован - `sessions/` содержит файлы из `observations/` - `insights.md` скопирован 4. `sessionCount` возвращает 2 (файлы `2026-W20.md`, `2026-W21.md`) 5. `generateIntro()` использует `hasHistory = true` и показывает контекст из W20/W21 --- ## Объём работы ~30 строк кода. Затрагивает 2 файла, нет API изменений, нет UI изменений.