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>
133 lines
8.8 KiB
Markdown
133 lines
8.8 KiB
Markdown
# 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>` в теле).
|
|
|
|
## Ссылки между заметками (`[[wikilinks]]`)
|
|
|
|
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 миграций без бэкапа волта.
|