[2026-05-25] psychologist-app: task for Obsidian import compatibility

This commit is contained in:
Alexey Martemyanov
2026-05-25 17:16:51 +06:00
parent 86a1b93f4e
commit d763450247
@@ -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 изменений.