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>
9.4 KiB
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.
Решённые принципы
- Идентичность — pageId, не имя файла. (Уже подпёрто фиксом
5133bb34: страница с известным pageId, чей файл переехал, никогда не удаляется — только move.) - Вся мета — в
.gitmost/index.json. Файлы.md— без меты. - Папка = страница-родитель, дерево зеркальное 1:1.
- Контент родителя — внутри его папки, файлом-индексом (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
{
"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)
- Прочитать дерево спейса (как сейчас) + текущий
.gitmost/index.json. - Построить layout: лист→
<t>.md, родитель→<t>/index.md, коллизии→~slug. - Записать ЧИСТЫЕ
.md(только тело) + обновитьindex.json(path→{pageId,slugId}). - Старые
index.md/файлы переехавших страниц — move (по pageId), не delete. - Коммит на
docmost, merge вmain(как сейчас).
PUSH (vault → Docmost)
- Дифф
last-pushed..main(как сейчас). - Идентичность файла берём из
index.json(path→pageId), НЕ из меты в файле. - Родитель — из пути (enclosing
<folder>/index.md→ его pageId из индекса). - Классификация:
- файл/папка ЕСТЬ в индексе → 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.
План фаз (каждая — тесты на одноразовом стенде + юниты движка)
- Индекс-модуль: чтение/запись
.gitmost/index.json(pure + IO-инъекция), юниты. Без смены поведения. - PULL пишет чистые файлы + индекс (folder-note layout), миграция со старого формата. Юниты layout + e2e: Docmost→git даёт чистый markdown, folder-note.
- PUSH читает индекс для идентичности/родителя (вместо меты в файле). e2e: push-правка/move/rename без потерь.
- Адопция: голый
.md/папка → страница. e2e: положить файл/папку через Obsidian-подобный клиент → появляются страницы. - Чистка: удалить
docmost:metaиз формата (оставить только фолбэк-парсер),.gitignoreдля.obsidian/.
Риски
Это смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня уже ловили
тяжёлый баг с трашем живых страниц). Поэтому: каждая фаза — за фиче-флагом/инкрем.,
с юнит-тестами движка И браузерным e2e (git-sync-browser-e2e.cjs) + изолированные
shell-e2e на одноразовом спейсе. Никаких «миграций на месте» без бэкапа волта.