Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 6c5cdbd002 feat(git-sync): native-Obsidian format — phase 1 = page-file (frontmatter gitmost_id)
Pivot the thin-meta design to "the vault IS a native Obsidian vault": clean
markdown + a minimal YAML frontmatter `gitmost_id:` (the durable pageId, travels
with the file so identity survives any move); folders mirror the page tree with
the parent's body as a folder-note `<Folder>/<Folder>.md` (LostPaul Folder Notes
convention); links as `[[wikilinks]]` (basename-resolved → reparent never breaks a
link, only retitle does); collisions disambiguated Obsidian-style; `.obsidian/`
and non-page files left untouched (no .gitignore). Verified the conventions
against the Obsidian/Folder-Notes docs.

Replaces the abandoned `.gitmost/index.json` sidecar (path-keyed → fragile to
git-undetected renames; the in-file id is self-sufficient): removes vault-index.ts.
Adds lib/page-file.ts — parsePageFile/serializePageFile (frontmatter id + clean
body) with a LEGACY `docmost:meta` fallback for migration. 6 unit tests; engine
suite green. Not yet wired into pull/push — no behavior change. Design doc
rewritten to the native-Obsidian format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 05:30:29 +03:00

8.8 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 в файлах.

  • На первом цикле нового движка: если у файла нет frontmatter, но есть docmost:meta → читаем pageId оттуда, переписываем файл в native-формат (frontmatter id + чистое тело + folder-note layout), разовый «normalize» коммит.
  • Фолбэк навсегда: docmost:meta всё ещё парсится как источник id, если frontmatter нет (файл со старой системы). (Реализовано в parsePageFile.)

Краевые случаи

  • 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 + тело, фолбэк на legacy docmost:meta). Юниты. Без смены поведения. (готово)
  2. PULL пишет native-формат (frontmatter + folder-note layout) + миграция.
  3. PUSH берёт идентичность из frontmatter, родителя из пути.
  4. Адопция голых файлов/папок.
  5. Чистка: убрать docmost:meta из генерации (оставить фолбэк-парсер).
  6. Ссылки: конвертер Docmost-mention ↔ [[wikilink]] + переписывание при retitle.

Риски

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