9.1 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 формат НЕ поддерживаем (данные тестовые). Волт — кэш: на
переходе rm -rf волты спейсов, они пересобираются из Docmost сразу в native-
формате. parsePageFile не читает docmost:meta; файл без gitmost_id frontmatter
— это голый/рукописный файл → адопция (не legacy-страница).
Краевые случаи
- 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 + тело,gitmost_idfrontmatter + тело). Юниты. Без смены поведения. (готово) - ✅ PULL пишет native-формат (frontmatter + folder-note layout). Волты
wipe+rebuild. (2a — folder-note layout в
buildVaultLayout; 2b — PULL пишетserializePageFile,readExistingчитает frontmatter.) (готово) - ✅ PUSH берёт идентичность из frontmatter, title из имени файла, родителя из
пути (
parentFolderFilefolder-note-aware). CREATE пишетgitmost_idобратно; UPDATE шлёт чистое тело (без frontmatter) на обе стороны 3-way merge. (готово) - Адопция голых файлов/папок (частично в фазе 3: файл без
gitmost_id→ create). - Чистка: выпилить старый
docmost:metaформат-код целиком. - Ссылки: конвертер Docmost-mention ↔
[[wikilink]]+ переписывание при retitle.
Риски
Смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня ловили тяжёлый баг
с трашем живых страниц). Каждая фаза — за инкрементом, с юнит-тестами движка И
браузерным e2e (git-sync-browser-e2e.cjs) + изолированными shell-e2e на
одноразовом спейсе. Без in-place миграций без бэкапа волта.