diff --git a/docs/backlog/git-sync-thin-meta.md b/docs/backlog/git-sync-thin-meta.md new file mode 100644 index 00000000..303ff306 --- /dev/null +++ b/docs/backlog/git-sync-thin-meta.md @@ -0,0 +1,138 @@ +# git-sync: thin meta + third-party-editor support + +Статус: **дизайн (согласован с владельцем 2026-06-24), к реализации.** + +## Зачем + +Сейчас каждый `.md` в волте несёт служебный блок +`` и +``. Сторонние редакторы (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). + +## Формат волта + +``` +/ + .gitmost/ + index.json # вся служебная мета (см. ниже) + Заметка.md # лист без детей: .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 на одноразовом спейсе. Никаких «миграций на месте» без бэкапа волта.