Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 b6bca5d8e1 docs(git-sync): design for thin meta + third-party-editor support
All service metadata moves into a single `.gitmost/index.json` sidecar; the `.md`
files become clean markdown (Obsidian & any editor work directly). The page tree
mirrors the folder structure (folder = parent page; the parent's body lives in
`<Folder>/index.md`); collisions disambiguate by a `~<slugId>` filename suffix
with identity tracked by pageId in the index (safe renames, never delete+create —
backed by 5133bb34). Bare files/folders from a third-party editor are adopted into
pages. Includes the migration path off the current `docmost:meta`-in-file format
and a phased plan (each phase gated by engine unit tests + the browser e2e +
isolated shell e2e). Agreed with the owner 2026-06-24.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:18:20 +03:00

139 lines
9.4 KiB
Markdown

# git-sync: thin meta + third-party-editor support
Статус: **дизайн (согласован с владельцем 2026-06-24), к реализации.**
## Зачем
Сейчас каждый `.md` в волте несёт служебный блок
`<!-- docmost:meta {version,pageId,slugId,title,spaceId,parentPageId} -->` и
`<!-- docmost:comments [...] -->`. Сторонние редакторы (Obsidian + obsidian-git —
основной кейс) про это не знают: они кладут «голые» markdown-файлы и папки, а
движок их МОЛЧА игнорирует (нет pageId → не страница), плюс мета сорит в каждом
файле.
Цель: `.md` остаются **чистым markdown** (любой редактор доволен), вся служебка
живёт в одном сайдкаре `.gitmost/index.json`, а дерево страниц **зеркалится
структурой папок**. Голые файлы/папки от стороннего редактора **адоптируются** в
страницы Docmost.
## Решённые принципы
1. **Идентичность — pageId, не имя файла.** (Уже подпёрто фиксом 5133bb34: страница
с известным pageId, чей файл переехал, никогда не удаляется — только move.)
2. **Вся мета — в `.gitmost/index.json`.** Файлы `.md` — без меты.
3. **Папка = страница-родитель**, дерево зеркальное 1:1.
4. **Контент родителя — внутри его папки**, файлом-индексом (folder-note).
## Формат волта
```
<Space-vault>/
.gitmost/
index.json # вся служебная мета (см. ниже)
Заметка.md # лист без детей: <title>.md, чистый markdown
Проект/ # страница-родитель = папка
index.md # ТЕЛО самой страницы «Проект» (folder-note)
Задача.md # ребёнок
Подпроект/
index.md # тело «Подпроект»
...
```
- **Лист** (нет детей) → `<title>.md`.
- **Родитель** (есть дети) → папка `<title>/`, тело в `<title>/index.md`.
Когда у листа появляется первый ребёнок — `<title>.md` превращается в
`<title>/index.md` (это безопасный move по pageId).
- **Имя файла** = санитизированный title + `.md`. Родитель чьё имя = имя папки.
- **Коллизии** (2+ сиблинга с одним title): всем коллидирующим — суффикс
` ~<slugId>` (`Заметка ~Cj7YX7.md`). slugId короткий и стабильный (из Docmost),
суффикс детерминирован по самой странице, не по порядку. Смена суффикса —
безопасный rename (идентичность в индексе по pageId).
- `index.md` внутри папки «занят» телом родителя; ребёнок с title «index»
получает `index ~slug.md`.
### `.gitmost/index.json`
```jsonc
{
"version": 1,
"spaceId": "019…", // спейс = эта репа (одна на спейс)
"pages": {
// ключ — vault-relative путь файла (forward-slash)
"Проект/index.md": { "pageId": "019…", "slugId": "Ab12Cd" },
"Проект/Задача.md": { "pageId": "019…", "slugId": "Ef34Gh" },
"Заметка ~Cj7YX7.md": { "pageId": "019…", "slugId": "Cj7YX7" }
}
}
```
- `title` — выводим из имени файла (stem), для папки — из имени папки.
- `parentPageId` — выводим из ПУТИ (родитель = `index.md` ближайшей родительской
папки; корневой файл → parent=null). Это уже path-as-truth на пуше сегодня.
- `version`/`updatedAt` — не храним: факт правки берётся из git.
- В индексе — только то, что НЕ выводимо: `pageId` (идентичность) и `slugId`
(дизамбигуация коллизий, write-back при create).
## PULL (Docmost → vault)
1. Прочитать дерево спейса (как сейчас) + текущий `.gitmost/index.json`.
2. Построить layout: лист→`<t>.md`, родитель→`<t>/index.md`, коллизии→` ~slug`.
3. Записать ЧИСТЫЕ `.md` (только тело) + обновить `index.json` (path→{pageId,slugId}).
4. Старые `index.md`/файлы переехавших страниц — move (по pageId), не delete.
5. Коммит на `docmost`, merge в `main` (как сейчас).
## PUSH (vault → Docmost)
1. Дифф `last-pushed..main` (как сейчас).
2. Идентичность файла берём из `index.json` (path→pageId), НЕ из меты в файле.
3. Родитель — из пути (enclosing `<folder>/index.md` → его pageId из индекса).
4. Классификация:
- файл/папка ЕСТЬ в индексе → update/move/rename по pageId (как сейчас, но
идентичность из индекса).
- файла НЕТ в индексе (новый голый файл) → **adopt**: create page
(title=stem, parent=из пути), записать pageId/slugId в индекс + (если
коллизия) переименовать файл с суффиксом.
- голая папка с детьми, но без своей записи → создать страницу-родитель
(пустое тело), завести `<folder>/index.md`, добавить в индекс.
- файл пропал, а pageId ещё в дереве/индексе под другим путём → move (фикс уже
есть). Реально пропал отовсюду → delete (как сейчас, под delete-cap).
## Миграция со старого формата
Существующие волты несут `docmost:meta` в файлах и НЕ имеют `.gitmost/index.json`.
- На первом цикле нового движка: если `.gitmost/index.json` нет — построить его из
`docmost:meta`, прочитанных по файлам, затем **переписать** файлы без меты
(один разовый «normalize» коммит) и разложить в folder-note layout.
- **Фолбэк навсегда**: если у файла нашлась `docmost:meta`, а в индексе записи нет
— уважаем мету (на случай файла, принесённого со старой системой).
## Краевые случаи
- Git не хранит пустые папки → «омит файла родителя» работает только пока есть
дети (папка жива за счёт детей). Childless пустая страница → свой `<t>.md`.
- `.gitmost/` и `.obsidian/` (и любые dot-папки) — игнор движком; добавить
дефолтный `.gitignore` для `.obsidian/` при инициализации волта.
- Конфликт `index.md` (folder-note) с ребёнком title «index» → ребёнку суффикс.
- Переименование папки (= rename родителя) → move всего поддерева, идентичность по
pageId из индекса, без delete+create.
## План фаз (каждая — тесты на одноразовом стенде + юниты движка)
1. **Индекс-модуль**: чтение/запись `.gitmost/index.json` (pure + IO-инъекция),
юниты. Без смены поведения.
2. **PULL пишет чистые файлы + индекс** (folder-note layout), миграция со старого
формата. Юниты layout + e2e: Docmost→git даёт чистый markdown, folder-note.
3. **PUSH читает индекс** для идентичности/родителя (вместо меты в файле). e2e:
push-правка/move/rename без потерь.
4. **Адопция**: голый `.md`/папка → страница. e2e: положить файл/папку через
Obsidian-подобный клиент → появляются страницы.
5. **Чистка**: удалить `docmost:meta` из формата (оставить только фолбэк-парсер),
`.gitignore` для `.obsidian/`.
## Риски
Это смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня уже ловили
тяжёлый баг с трашем живых страниц). Поэтому: каждая фаза — за фиче-флагом/инкрем.,
с юнит-тестами движка И браузерным e2e (`git-sync-browser-e2e.cjs`) + изолированные
shell-e2e на одноразовом спейсе. Никаких «миграций на месте» без бэкапа волта.