diff --git a/personal/projects/psychologist-app/task-obsidian-import-compat.md b/personal/projects/psychologist-app/task-obsidian-import-compat.md new file mode 100644 index 00000000..f5710ecd --- /dev/null +++ b/personal/projects/psychologist-app/task-obsidian-import-compat.md @@ -0,0 +1,147 @@ +--- +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 изменений.