Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 6baad935f9 docs(git-sync): mark thin-meta phases 2 + 3 done in the plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:10:10 +03:00

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. (Ключ namespaced gitmost_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> в теле).

Obsidian резолвит [[Заметка]] по basename (не по полному пути), нормализуя пробелы/-/_, с приоритетом короткого пути при неоднозначности.

  • В Docmost ссылки — по pageId (mention/reference node), rename переживают.
  • В волте — обсидиановские [[basename]].
  • Следствие: reparent (смена папки) ссылку НЕ ломает (basename тот же), ломает только retitle. Значит переписывать [[…]] надо только при смене имени страницы — узкий случай. (Obsidian сам умеет «update links on rename».)
  • Конвертер Docmost-mention ↔ [[wikilink]] (обе стороны) + переписывание при retitle — отдельная фаза (см. план), не блокирует формат.

PULL (Docmost → vault)

  1. Прочитать дерево спейса.
  2. Layout: лист→<t>.md, родитель→<t>/<t>.md, коллизии→ 2/ 3.
  3. Записать ---\ngitmost_id: …\n---\n<тело> (чистый markdown).
  4. Переехавшие файлы — move (по id), не delete.
  5. Коммит на docmost, merge в main.

PUSH (vault → Docmost)

  1. Дифф last-pushed..main.
  2. Идентичность файла — из frontmatter gitmost_id. Родитель — из пути (folder-note родительской папки).
  3. Классификация:
    • есть 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)

  1. Формат файла: parsePageFile/serializePageFile (frontmatter id + тело, gitmost_id frontmatter + тело). Юниты. Без смены поведения. (готово)
  2. PULL пишет native-формат (frontmatter + folder-note layout). Волты wipe+rebuild. (2a — folder-note layout в buildVaultLayout; 2b — PULL пишет serializePageFile, readExisting читает frontmatter.) (готово)
  3. PUSH берёт идентичность из frontmatter, title из имени файла, родителя из пути (parentFolderFile folder-note-aware). CREATE пишет gitmost_id обратно; UPDATE шлёт чистое тело (без frontmatter) на обе стороны 3-way merge. (готово)
  4. Адопция голых файлов/папок (частично в фазе 3: файл без gitmost_id → create).
  5. Чистка: выпилить старый docmost:meta формат-код целиком.
  6. Ссылки: конвертер Docmost-mention ↔ [[wikilink]] + переписывание при retitle.

Риски

Смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня ловили тяжёлый баг с трашем живых страниц). Каждая фаза — за инкрементом, с юнит-тестами движка И браузерным e2e (git-sync-browser-e2e.cjs) + изолированными shell-e2e на одноразовом спейсе. Без in-place миграций без бэкапа волта.