kin: KIN-021 Аудит-лог для --dangerously-skip-permissions в auto mode
This commit is contained in:
parent
67071c757d
commit
a0b0976d8d
16 changed files with 1477 additions and 14 deletions
266
tasks/KIN-013-spec.md
Normal file
266
tasks/KIN-013-spec.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
# KIN-013 — Settings + Obsidian Sync: Техническая спецификация
|
||||
|
||||
## Контекст
|
||||
|
||||
Фича добавляет:
|
||||
1. Страницу Settings в GUI для управления конфигурацией проектов
|
||||
2. Двусторонний Obsidian sync: decisions → .md-файлы, чекбоксы Obsidian → статус задач
|
||||
|
||||
Sync вызывается явно по кнопке (не демон), через API-эндпоинт.
|
||||
|
||||
---
|
||||
|
||||
## 1. Схема данных
|
||||
|
||||
### Изменение таблицы `projects`
|
||||
|
||||
Добавить колонку:
|
||||
```sql
|
||||
ALTER TABLE projects ADD COLUMN obsidian_vault_path TEXT;
|
||||
```
|
||||
|
||||
**Миграция**: в `core/db.py` → `_migrate()`, по паттерну существующих миграций:
|
||||
```python
|
||||
if "obsidian_vault_path" not in proj_cols:
|
||||
conn.execute("ALTER TABLE projects ADD COLUMN obsidian_vault_path TEXT")
|
||||
conn.commit()
|
||||
```
|
||||
|
||||
**Семантика**: путь к корневой папке Obsidian vault для данного проекта.
|
||||
Пример: `/Users/grosfrumos/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault`
|
||||
|
||||
---
|
||||
|
||||
## 2. Формат .md-файлов для decisions
|
||||
|
||||
### Расположение
|
||||
```
|
||||
{vault_path}/{project_id}/decisions/{id}-{slug}.md
|
||||
```
|
||||
Пример: `.../kin/decisions/42-proxy-jump-ssh-gotcha.md`
|
||||
|
||||
### Формат файла (YAML frontmatter + Markdown body)
|
||||
```markdown
|
||||
---
|
||||
kin_decision_id: 42
|
||||
project: kin
|
||||
type: gotcha
|
||||
category: testing
|
||||
tags: [testing, mock, subprocess]
|
||||
created_at: 2026-03-10
|
||||
---
|
||||
|
||||
# Proxy через SSH не работает без ssh-agent
|
||||
|
||||
Описание: полный текст description из БД.
|
||||
```
|
||||
|
||||
**Обоснование frontmatter**:
|
||||
- Позволяет идентифицировать файл при импорте (поле `kin_decision_id`)
|
||||
- Позволяет Obsidian показывать метаданные в Properties panel
|
||||
- Поддерживает round-trip sync без парсинга имени файла
|
||||
|
||||
### Slug из заголовка
|
||||
```python
|
||||
import re
|
||||
def _slug(title: str) -> str:
|
||||
s = title.lower()
|
||||
s = re.sub(r"[^a-zа-я0-9\s-]", "", s)
|
||||
s = re.sub(r"\s+", "-", s.strip())
|
||||
return s[:50]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Механизм двустороннего sync
|
||||
|
||||
### 3.1 Decisions → Obsidian (export)
|
||||
|
||||
- Создать/перезаписать `.md`-файл для каждого decision проекта
|
||||
- Директория создаётся автоматически (`mkdir -p`)
|
||||
- Если файл для `kin_decision_id` уже существует — перезаписать (идемпотентно)
|
||||
- Решения, удалённые из БД → файлы НЕ удаляются (безопасно)
|
||||
|
||||
### 3.2 Obsidian чекбоксы → Tasks (import)
|
||||
|
||||
**Источник**: файлы `*.md` в `{vault_path}/{project_id}/tasks/`
|
||||
Дополнительно: файлы `{vault_path}/{project_id}/*.md`
|
||||
|
||||
**Формат строки задачи**:
|
||||
```
|
||||
- [x] KIN-013 Title of the task
|
||||
- [ ] KIN-014 Another task
|
||||
```
|
||||
|
||||
**Алгоритм**:
|
||||
1. Найти строки по паттерну: `^[-*]\s+\[([xX ])\]\s+([A-Z]+-\d+)\s+(.+)$`
|
||||
2. Извлечь: `done` (bool), `task_id` (str), `title` (str)
|
||||
3. Найти задачу в БД по `task_id`
|
||||
4. Если `done=True` и `task.status != 'done'` → `update_task(conn, task_id, status='done')`
|
||||
5. Если `done=False` → не трогать (не откатываем)
|
||||
6. Если задача не найдена → пропустить (не создавать)
|
||||
|
||||
**Обоснование**: строгий маппинг только по task_id исключает случайное создание мусора.
|
||||
|
||||
### 3.3 Функция `sync_obsidian`
|
||||
|
||||
```python
|
||||
def sync_obsidian(conn, project_id: str) -> dict:
|
||||
"""
|
||||
Returns:
|
||||
{
|
||||
"exported_decisions": int,
|
||||
"tasks_updated": int,
|
||||
"errors": list[str],
|
||||
"vault_path": str
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Модуль `core/obsidian_sync.py`
|
||||
|
||||
### Публичный API модуля
|
||||
|
||||
```python
|
||||
def export_decisions_to_md(
|
||||
project_id: str,
|
||||
decisions: list[dict],
|
||||
vault_path: Path,
|
||||
) -> list[Path]:
|
||||
"""Экспортирует decisions в .md файлы Obsidian. Возвращает список созданных файлов."""
|
||||
|
||||
def parse_task_checkboxes(
|
||||
vault_path: Path,
|
||||
project_id: str,
|
||||
) -> list[dict]:
|
||||
"""Парсит *.md файлы в vault/{project_id}/tasks/ на чекбоксы с task ID.
|
||||
Returns: [{"task_id": "KIN-013", "done": True, "title": "..."}]
|
||||
"""
|
||||
|
||||
def sync_obsidian(conn, project_id: str) -> dict:
|
||||
"""Оркестратор: export + import. Возвращает статистику."""
|
||||
```
|
||||
|
||||
### Вспомогательные (приватные)
|
||||
|
||||
```python
|
||||
def _slug(title: str) -> str # slug для имени файла
|
||||
def _decision_to_md(decision: dict) -> str # форматирует .md с frontmatter
|
||||
def _parse_frontmatter(text: str) -> dict # для будущего round-trip
|
||||
```
|
||||
|
||||
### Зависимости
|
||||
- Только стандартная библиотека Python: `pathlib`, `re`, `yaml` (через `import yaml`) или ручной YAML-парсер
|
||||
- Важно: PyYAML может не быть установлен → использовать простой ручной вывод YAML-фронтматтера, парсинг через `re`
|
||||
- Импортирует из `core.models`: `get_project`, `get_decisions`, `get_task`, `update_task`
|
||||
- Импортирует из `core.db`: `get_connection` — НЕ нужен, conn передаётся снаружи
|
||||
|
||||
---
|
||||
|
||||
## 5. API-эндпоинты
|
||||
|
||||
### 5.1 PATCH /api/projects/{project_id} — расширить
|
||||
|
||||
Добавить в `ProjectPatch`:
|
||||
```python
|
||||
class ProjectPatch(BaseModel):
|
||||
execution_mode: str | None = None
|
||||
autocommit_enabled: bool | None = None
|
||||
obsidian_vault_path: str | None = None # новое поле
|
||||
```
|
||||
|
||||
Обновить обработчик: если `obsidian_vault_path` provided → `update_project(conn, id, obsidian_vault_path=...)`
|
||||
Убрать проверку "Nothing to update" → включить `obsidian_vault_path` в условие.
|
||||
|
||||
### 5.2 POST /api/projects/{project_id}/sync/obsidian — новый
|
||||
|
||||
```python
|
||||
@app.post("/api/projects/{project_id}/sync/obsidian")
|
||||
def sync_obsidian_endpoint(project_id: str):
|
||||
conn = get_conn()
|
||||
p = models.get_project(conn, project_id)
|
||||
if not p:
|
||||
conn.close()
|
||||
raise HTTPException(404, ...)
|
||||
if not p.get("obsidian_vault_path"):
|
||||
conn.close()
|
||||
raise HTTPException(400, "obsidian_vault_path not set for this project")
|
||||
from core.obsidian_sync import sync_obsidian
|
||||
result = sync_obsidian(conn, project_id)
|
||||
conn.close()
|
||||
return result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend: Settings страница
|
||||
|
||||
### Маршрут
|
||||
- Path: `/settings`
|
||||
- Component: `web/frontend/src/views/SettingsView.vue`
|
||||
- Регистрация в `main.ts`
|
||||
|
||||
### Навигация в `App.vue`
|
||||
Добавить ссылку `Settings` в хедер рядом с `Kin`.
|
||||
|
||||
### SettingsView.vue — структура
|
||||
|
||||
```
|
||||
Settings
|
||||
├── Список проектов (v-for)
|
||||
│ ├── Название + id
|
||||
│ ├── Input: obsidian_vault_path (text input)
|
||||
│ ├── [Save] кнопка → PATCH /api/projects/{id}
|
||||
│ └── [Sync Obsidian] кнопка → POST /api/projects/{id}/sync/obsidian
|
||||
│ └── Показывает результат: "Exported: 5 decisions, Updated: 2 tasks"
|
||||
```
|
||||
|
||||
### api.ts — добавить методы
|
||||
|
||||
```typescript
|
||||
// Обновить настройки проекта
|
||||
patchProject(id: string, data: { obsidian_vault_path?: string, execution_mode?: string, autocommit_enabled?: boolean })
|
||||
|
||||
// Запустить Obsidian sync
|
||||
syncObsidian(projectId: string): Promise<{ exported_decisions: number, tasks_updated: number, errors: string[] }>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Тесты
|
||||
|
||||
### `tests/test_obsidian_sync.py`
|
||||
|
||||
Обязательные кейсы:
|
||||
1. `test_export_decisions_creates_md_files` — export создаёт файлы с правильным frontmatter
|
||||
2. `test_export_idempotent` — повторный export перезаписывает, не дублирует
|
||||
3. `test_parse_task_checkboxes_done` — `- [x] KIN-001 Title` → `{"task_id": "KIN-001", "done": True}`
|
||||
4. `test_parse_task_checkboxes_pending` — `- [ ] KIN-002 Title` → `done: False`
|
||||
5. `test_parse_task_checkboxes_no_id` — строки без task ID пропускаются
|
||||
6. `test_sync_updates_task_status` — sync обновляет статус задачи если `done=True`
|
||||
7. `test_sync_no_vault_path` — sync без vault_path выбрасывает ValueError
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски и ограничения
|
||||
|
||||
1. **PyYAML не в зависимостях** → использовать ручную генерацию YAML-строки для frontmatter, парсинг `re`
|
||||
2. **Vault path может быть недоступен** → sync возвращает error в `errors[]`, не падает
|
||||
3. **Конфликт при rename decision** → файл со старым slug остаётся, создаётся новый. Приемлемо для v1
|
||||
4. **Большой vault** → scan только в `{vault_path}/{project_id}/tasks/`, не весь vault
|
||||
5. **Одновременный sync** → нет блокировки (SQLite WAL + file system). В v1 достаточно
|
||||
|
||||
---
|
||||
|
||||
## 9. Порядок реализации (для dev-агента)
|
||||
|
||||
1. `core/db.py` — добавить `obsidian_vault_path` в `_migrate()`
|
||||
2. `core/obsidian_sync.py` — реализовать `export_decisions_to_md`, `parse_task_checkboxes`, `sync_obsidian`
|
||||
3. `tests/test_obsidian_sync.py` — написать тесты (7 кейсов выше)
|
||||
4. `web/api.py` — расширить `ProjectPatch`, добавить `/sync/obsidian` эндпоинт
|
||||
5. `web/frontend/src/api.ts` — добавить `patchProject` обновление и `syncObsidian`
|
||||
6. `web/frontend/src/views/SettingsView.vue` — создать компонент
|
||||
7. `web/frontend/src/main.ts` — зарегистрировать `/settings` маршрут
|
||||
8. `web/frontend/src/App.vue` — добавить ссылку Settings в nav
|
||||
Loading…
Add table
Add a link
Reference in a new issue