Files
gitmost/docs/backlog/git-sync-thin-meta.md
claude code agent 227 3b00cd5021 fix(git-sync): screen non-page files out of PUSH (CRITICAL — review)
Self-review of phase 3 caught a data-corruption regression: nativeMeta always
supplies the run's spaceId, so the planner's 'create-without-spaceId' skip — which
had doubled as the only filter for non-page files — went dead. An ADDED
.obsidian/*.json, attachment, or dotfile (committed to the vault, no .gitignore)
would then be classified as a CREATE: a junk Docmost page, plus a gitmost_id
frontmatter written INTO the file, corrupting it.

Fix: isPageFile(path) — a .md file with NO dot-segment anywhere — and filter the
diff to page files at the very top of computePushActions, BEFORE any
classification, so non-page A/M/D/R are ignored (design §Адопция). 2 unit tests
pin it (.obsidian/json, attachment, dotfile, dot-segment, .md dotfile all ignored;
real pages still created). 614 engine tests green.

Also: refreshed stale docmost:meta comments to gitmost_id (review SUGGESTION), and
documented the deferred adoption frontmatter-preservation gap (review WARNING) in
page-file.ts + the design doc (do NOT roll native onto a real vault with Obsidian
properties until phase 4 round-trips them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:08:29 +03:00

140 lines
9.7 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` формат НЕ поддерживаем (данные тестовые). Волт — кэш: на
переходе `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).
ВАЖНО: тут же сохранить пользовательский frontmatter (Obsidian properties) при
адопции — `parsePageFile` сейчас срезает ведущий frontmatter даже без
`gitmost_id`, а write-back пишет только `gitmost_id`; нужно врезать `gitmost_id`
в существующий frontmatter и сохранять остальные поля И при write-back, И при
следующем pull (иначе pull перезатрёт). До этого native-формат НЕ катить на
реальный Obsidian-волт с properties.
5. Чистка: выпилить старый `docmost:meta` формат-код целиком.
6. Ссылки: конвертер Docmost-mention ↔ `[[wikilink]]` + переписывание при retitle.
## Риски
Смена ФОРМАТА волта на data-loss-чувствительном движке (сегодня ловили тяжёлый баг
с трашем живых страниц). Каждая фаза — за инкрементом, с юнит-тестами движка И
браузерным e2e (`git-sync-browser-e2e.cjs`) + изолированными shell-e2e на
одноразовом спейсе. Без in-place миграций без бэкапа волта.