Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 a91405632e feat(git-sync): native-Obsidian format — phase 1 = page-file (frontmatter gitmost_id)
Pivot the thin-meta design to "the vault IS a native Obsidian vault": clean
markdown + a minimal YAML frontmatter `gitmost_id:` (the durable pageId, travels
with the file so identity survives any move); folders mirror the page tree with
the parent's body as a folder-note `<Folder>/<Folder>.md` (LostPaul Folder Notes
convention); links as `[[wikilinks]]` (basename-resolved → reparent never breaks a
link, only retitle does); collisions disambiguated Obsidian-style; `.obsidian/`
and non-page files left untouched (no .gitignore). Verified the conventions
against the Obsidian/Folder-Notes docs.

Replaces the abandoned `.gitmost/index.json` sidecar (path-keyed → fragile to
git-undetected renames; the in-file id is self-sufficient): removes vault-index.ts.
Adds lib/page-file.ts — parsePageFile/serializePageFile (frontmatter id + clean
body) with a LEGACY `docmost:meta` fallback for migration. 6 unit tests; engine
suite green. Not yet wired into pull/push — no behavior change. Design doc
rewritten to the native-Obsidian format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:10:10 +03:00

133 lines
8.8 KiB
Markdown

# git-sync: native-Obsidian vault format
Статус: **дизайн (согласован с владельцем 2026-06-24), к реализации.**
## Цель
Волт спейса должен быть **настоящим Obsidian-волтом**: владелец открывает папку в
Obsidian (с плагином Folder Notes) и получает ровно ту же структуру страниц, не
замечая разницы. Никаких служебных артефактов, которые бы выглядели чужеродно.
Сторонние редакторы кладут «голые» файлы/папки — движок их **адоптирует** в
страницы Docmost.
Сейчас каждый `.md` несёт жирный `<!-- docmost:meta {…} -->` блок — это уезжает.
## Формат
```
<Space-vault>/
Заметка.md # лист: чистый markdown + frontmatter id
Проект/ # страница-родитель = ПАПКА
Проект.md # folder-note: ТЕЛО самой страницы «Проект»
Задача.md # ребёнок
Подпроект/
Подпроект.md # тело «Подпроект»
...
.obsidian/ # конфиг Obsidian — движок НЕ ТРОГАЕТ
```
Каждый файл страницы:
```
---
gitmost_id: 019ef6fc-2638-7ce1-9ce3-2756ce038480
---
<чистый markdown — тело страницы (wiki-ссылки, всё как в Obsidian)>
```
- **Лист** (нет детей) → `<title>.md`.
- **Родитель** (есть дети) → папка `<title>/`, его тело в `<title>/<title>.md`
(folder-note по конвенции плагина LostPaul Folder Notes — заметка с именем
папки внутри неё). Лист, у которого появился первый ребёнок, превращается из
`<title>.md` в `<title>/<title>.md` (безопасный move по id).
- **title** = имя файла (для папки — имя папки). **parentPageId** = ближайшая
родительская папка (её folder-note). **spaceId** = эта репа. Всё выводимо.
- **Идентичность** — `gitmost_id` (= Docmost pageId) во frontmatter. Невыводима,
едет ВМЕСТЕ с файлом → переживает любой move, даже не распознанный git как
rename. (Ключ namespaced `gitmost_id`, не голый `id`, чтобы не конфликтовать с
пользовательскими frontmatter-полями. Имя ключа — последнее на подтверждении.)
- **Коллизии имён** (2+ сиблинга с одним title): как делает сам Obsidian —
добавляем натуральный суффикс ` 2`, ` 3`. id во frontmatter, так что имя файла
чисто косметическое; смена суффикса — безопасный rename (идентичность по id).
Никакого `.gitmost/index.json` (сайдкар отвергнут: path-keyed индекс хрупок к
rename; id во frontmatter самодостаточен). Никаких `docmost:meta`/`docmost:comments`
блоков (комменты и так живут инлайн-марками `<span data-comment-id>` в теле).
## Ссылки между заметками (`[[wikilinks]]`)
Obsidian резолвит `[[Заметка]]` по **basename** (не по полному пути), нормализуя
пробелы/`-`/`_`, с приоритетом короткого пути при неоднозначности.
- В Docmost ссылки — по pageId (mention/reference node), rename переживают.
- В волте — обсидиановские `[[basename]]`.
- Следствие: **reparent (смена папки) ссылку НЕ ломает** (basename тот же),
ломает только **retitle**. Значит переписывать `[[…]]` надо только при смене
имени страницы — узкий случай. (Obsidian сам умеет «update links on rename».)
- Конвертер Docmost-mention ↔ `[[wikilink]]` (обе стороны) + переписывание при
retitle — **отдельная фаза** (см. план), не блокирует формат.
## PULL (Docmost → vault)
1. Прочитать дерево спейса.
2. Layout: лист→`<t>.md`, родитель→`<t>/<t>.md`, коллизии→` 2`/` 3`.
3. Записать `---\ngitmost_id: …\n---\n<тело>` (чистый markdown).
4. Переехавшие файлы — move (по id), не delete.
5. Коммит на `docmost`, merge в `main`.
## PUSH (vault → Docmost)
1. Дифф `last-pushed..main`.
2. Идентичность файла — из frontmatter `gitmost_id`. Родитель — из пути (folder-note
родительской папки).
3. Классификация:
- есть `gitmost_id` в дереве → update/move/rename по id (страховка 5133bb34).
- нет id (новый голый файл от Obsidian) → **adopt**: create page (title=имя,
parent=папка), дописать `gitmost_id` во frontmatter.
- голая папка с детьми без folder-note → создать страницу-родитель, завести
`<folder>/<folder>.md`.
- файл пропал, а id ещё в дереве под другим путём → move. Реально пропал →
delete (под delete-cap).
## Адопция (третья-сторона → Docmost)
- голый `.md` без frontmatter id → create page.
- голая папка с `.md` внутри без folder-note → create страницу-родитель + folder-note.
- `.obsidian/`, аттачменты, dot-файлы, любые не-`.md`**игнор** (не страницы),
лежат в гите как есть, Obsidian ими владеет. Без `.gitignore`.
## Миграция со старого формата
Существующие волты несут `docmost:meta` в файлах.
- На первом цикле нового движка: если у файла нет frontmatter, но есть
`docmost:meta` → читаем pageId оттуда, переписываем файл в native-формат
(frontmatter id + чистое тело + folder-note layout), разовый «normalize» коммит.
- **Фолбэк навсегда**: `docmost:meta` всё ещё парсится как источник id, если
frontmatter нет (файл со старой системы). (Реализовано в `parsePageFile`.)
## Краевые случаи
- Git не хранит пустые папки → «родитель без своего файла» невозможен: тело
родителя — это folder-note `<t>/<t>.md`, он и держит папку (плюс дети). Childless
пустая страница → просто `<t>.md`.
- Конфликт folder-note `Папка/Папка.md` с ребёнком title «Папка» → ребёнку суффикс.
- Переименование папки (= rename родителя) → move всего поддерева по id, без
delete+create; ссылки `[[…]]` на сам родитель переписать (basename сменился).
## План фаз (каждая — юниты движка + браузерный e2e + изолированные shell-e2e)
1. ✅ Формат файла: `parsePageFile`/`serializePageFile` (frontmatter id + тело,
фолбэк на legacy `docmost:meta`). Юниты. Без смены поведения. (готово)
2. PULL пишет native-формат (frontmatter + folder-note layout) + миграция.
3. PUSH берёт идентичность из frontmatter, родителя из пути.
4. Адопция голых файлов/папок.
5. Чистка: убрать `docmost:meta` из генерации (оставить фолбэк-парсер).
6. Ссылки: конвертер Docmost-mention ↔ `[[wikilink]]` + переписывание при retitle.
## Риски
Смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня ловили тяжёлый баг
с трашем живых страниц). Каждая фаза — за инкрементом, с юнит-тестами движка И
браузерным e2e (`git-sync-browser-e2e.cjs`) + изолированными shell-e2e на
одноразовом спейсе. Без in-place миграций без бэкапа волта.