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>
8.8 KiB
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. (Ключ namespacedgitmost_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)
- Прочитать дерево спейса.
- Layout: лист→
<t>.md, родитель→<t>/<t>.md, коллизии→2/3. - Записать
---\ngitmost_id: …\n---\n<тело>(чистый markdown). - Переехавшие файлы — move (по id), не delete.
- Коммит на
docmost, merge вmain.
PUSH (vault → Docmost)
- Дифф
last-pushed..main. - Идентичность файла — из frontmatter
gitmost_id. Родитель — из пути (folder-note родительской папки). - Классификация:
- есть
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)
- ✅ Формат файла:
parsePageFile/serializePageFile(frontmatter id + тело, фолбэк на legacydocmost:meta). Юниты. Без смены поведения. (готово) - PULL пишет native-формат (frontmatter + folder-note layout) + миграция.
- PUSH берёт идентичность из frontmatter, родителя из пути.
- Адопция голых файлов/папок.
- Чистка: убрать
docmost:metaиз генерации (оставить фолбэк-парсер). - Ссылки: конвертер Docmost-mention ↔
[[wikilink]]+ переписывание при retitle.
Риски
Смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня ловили тяжёлый баг
с трашем живых страниц). Каждая фаза — за инкрементом, с юнит-тестами движка И
браузерным e2e (git-sync-browser-e2e.cjs) + изолированными shell-e2e на
одноразовом спейсе. Без in-place миграций без бэкапа волта.