Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 ca60b8f692 docs(git-sync): design for thin meta + third-party-editor support
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>
2026-06-24 16:50:17 +03:00

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.

Решённые принципы

  1. Идентичность — pageId, не имя файла. (Уже подпёрто фиксом 5133bb34: страница с известным pageId, чей файл переехал, никогда не удаляется — только move.)
  2. Вся мета — в .gitmost/index.json. Файлы .md — без меты.
  3. Папка = страница-родитель, дерево зеркальное 1:1.
  4. Контент родителя — внутри его папки, файлом-индексом (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)

  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 на одноразовом спейсе. Никаких «миграций на месте» без бэкапа волта.