feat(git-sync): native-Obsidian vault format (thin meta) — phases 1–2 #148

Closed
Ghost wants to merge 45 commits from feat/git-sync-thin-meta into feat/git-sync

Что это

Редизайн on-disk формата git-sync-волта: волт спейса становится настоящим Obsidian-волтом. Цель — открыть папку волта в Obsidian (с плагином Folder Notes) и получить ровно ту же структуру страниц, не заметив разницы. Никаких служебных артефактов в файлах.

Раньше каждый .md нёс жирный <!-- docmost:meta {…} --> блок. Теперь — чистый markdown с минимальной YAML-метой.

Принятые архитектурные решения

1. Идентичность — gitmost_id во frontmatter (вариант C).
Единственное, что невыводимо из структуры — это Docmost pageId. Кладём его в минимальный YAML-frontmatter:

---
gitmost_id: 019ef6fc-2638-7ce1-9ce3-2756ce038480
---
<чистый markdown>
  • Ключ namespaced (gitmost_id, не голый id) — чтобы не конфликтовать с пользовательскими frontmatter-полями.
  • id едет вместе с файлом → переживает любой move, даже не распознанный git как rename (это чинит класс data-loss багов: идентичность по id, не по имени файла).
  • Отвергли: сайдкар .gitmost/index.json (path-keyed индекс хрупок к rename); голый docmost:meta (чужероден в Obsidian).

2. Вся остальная мета — выводима, в файле её нет.
title = имя файла. parentPageId = родительская папка (по пути). spaceId = репозиторий. updatedAt = git. В файле — только gitmost_id.

3. Папки = страницы-родители, тело — в folder-note.
Структура дерева зеркальна. Страница С детьми → папка Имя/, а её собственное тело — в Имя/Имя.md (folder-note, конвенция плагина LostPaul Folder Notes). Дети лежат рядом в той же папке. Лист → просто Имя.md.

  • folder-note забирает канонический путь раньше одноимённого ребёнка: суффикс получает ребёнок-лист, не папка (папка X/ всегда содержит свою заметку X).

4. Ссылки — Obsidian [[wikilinks]] по basename.
Obsidian резолвит [[Имя]] по basename, не по пути. Следствие: reparent (смена папки) ссылку НЕ ломает, ломает только retitle. Конвертер Docmost-mention ↔ [[wikilink]] + переписывание при retitle — отдельная фаза (не блокирует формат).

5. Без обратной совместимости / без миграции.
Волт — это кэш. На переходе rm -rf волты, они пересобираются из Docmost сразу в native-формате. parsePageFile не читает старый docmost:meta; файл без gitmost_id — это голый/рукописный файл → адопция (create page).

6. Без .gitignore.
.obsidian/, аттачменты, не-.md файлы — Obsidian ими владеет, движок не трогает (игнорирует как не-страницы, но в гите они лежат как есть).

Риск

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

Статус по фазам

  • Фаза 1 — формат файла (parsePageFile/serializePageFile, gitmost_id frontmatter). Юниты.
  • Фаза 2a — folder-note layout (buildVaultLayout: родитель → Папка/Папка.md). 6 юнитов.
  • Фаза 2b — PULL пишет native frontmatter (+ regression-guard в apply-pull тесте).
  • Фаза 3 — PUSH читает gitmost_id, title из имени файла, родитель из пути (folder-note aware parentFolderFile). (в работе)
  • Фаза 4 — адопция голых файлов/папок.
  • Фаза 5 — чистка: выпилить старый docmost:meta код.
  • Фаза 6 — ссылки: конвертер mention ↔ [[wikilink]] + переписывание при retitle.

Дизайн целиком: docs/backlog/git-sync-thin-meta.md.

Стек: на feat/git-sync (#119), т.к. строится поверх движка из той PR.

🤖 Generated with Claude Code

## Что это Редизайн on-disk формата git-sync-волта: волт спейса становится **настоящим Obsidian-волтом**. Цель — открыть папку волта в Obsidian (с плагином Folder Notes) и получить ровно ту же структуру страниц, не заметив разницы. Никаких служебных артефактов в файлах. Раньше каждый `.md` нёс жирный `<!-- docmost:meta {…} -->` блок. Теперь — чистый markdown с минимальной YAML-метой. ## Принятые архитектурные решения **1. Идентичность — `gitmost_id` во frontmatter (вариант C).** Единственное, что невыводимо из структуры — это Docmost `pageId`. Кладём его в минимальный YAML-frontmatter: ``` --- gitmost_id: 019ef6fc-2638-7ce1-9ce3-2756ce038480 --- <чистый markdown> ``` - Ключ **namespaced** (`gitmost_id`, не голый `id`) — чтобы не конфликтовать с пользовательскими frontmatter-полями. - id **едет вместе с файлом** → переживает любой move, даже не распознанный git как rename (это чинит класс data-loss багов: идентичность по id, не по имени файла). - Отвергли: сайдкар `.gitmost/index.json` (path-keyed индекс хрупок к rename); голый `docmost:meta` (чужероден в Obsidian). **2. Вся остальная мета — выводима, в файле её нет.** `title` = имя файла. `parentPageId` = родительская папка (по пути). `spaceId` = репозиторий. `updatedAt` = git. В файле — только `gitmost_id`. **3. Папки = страницы-родители, тело — в folder-note.** Структура дерева зеркальна. Страница С детьми → папка `Имя/`, а её собственное тело — в `Имя/Имя.md` (folder-note, конвенция плагина LostPaul Folder Notes). Дети лежат рядом в той же папке. Лист → просто `Имя.md`. - folder-note забирает канонический путь раньше одноимённого ребёнка: суффикс получает ребёнок-лист, не папка (папка `X/` всегда содержит свою заметку `X`). **4. Ссылки — Obsidian `[[wikilinks]]` по basename.** Obsidian резолвит `[[Имя]]` по basename, не по пути. Следствие: reparent (смена папки) ссылку НЕ ломает, ломает только retitle. Конвертер Docmost-mention ↔ `[[wikilink]]` + переписывание при retitle — **отдельная фаза** (не блокирует формат). **5. Без обратной совместимости / без миграции.** Волт — это кэш. На переходе `rm -rf` волты, они пересобираются из Docmost сразу в native-формате. `parsePageFile` не читает старый `docmost:meta`; файл без `gitmost_id` — это голый/рукописный файл → адопция (create page). **6. Без `.gitignore`.** `.obsidian/`, аттачменты, не-`.md` файлы — Obsidian ими владеет, движок не трогает (игнорирует как не-страницы, но в гите они лежат как есть). ## Риск Смена ФОРМАТА на data-loss-чувствительном движке (в этой же сессии ловили тяжёлый баг с трашем живых страниц). Поэтому фазируем, и каждая фаза — юнит-тесты движка + браузерный e2e + изолированные shell-e2e на одноразовом спейсе. ## Статус по фазам - [x] **Фаза 1** — формат файла (`parsePageFile`/`serializePageFile`, `gitmost_id` frontmatter). Юниты. - [x] **Фаза 2a** — folder-note layout (`buildVaultLayout`: родитель → `Папка/Папка.md`). 6 юнитов. - [x] **Фаза 2b** — PULL пишет native frontmatter (+ regression-guard в apply-pull тесте). - [ ] **Фаза 3** — PUSH читает `gitmost_id`, title из имени файла, родитель из пути (folder-note aware `parentFolderFile`). *(в работе)* - [ ] **Фаза 4** — адопция голых файлов/папок. - [ ] **Фаза 5** — чистка: выпилить старый `docmost:meta` код. - [ ] **Фаза 6** — ссылки: конвертер mention ↔ `[[wikilink]]` + переписывание при retitle. Дизайн целиком: `docs/backlog/git-sync-thin-meta.md`. Стек: на `feat/git-sync` (#119), т.к. строится поверх движка из той PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 7 commits 2026-06-24 04:46:53 +03:00
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>
Pure read/write/lookup for the vault sidecar index that will hold page identity
(pageId) + collision token (slugId) keyed by file path, so the .md files can be
clean markdown. parseVaultIndex is tolerant (missing/garbage/bad entries degrade
to empty/skipped — never crashes a cycle); serializeVaultIndex is deterministic
(sorted keys -> stable diffs, no churn). Lookups (pageIdAt, pathForPageId reverse,
trackedPageIds) + mutations (set/remove/move). NOT wired into pull/push yet — no
behavior change. 5 unit tests; engine suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the design discussion: a path-keyed sidecar is NOT a safe source of
truth (a git-undetected rename loses the page), so the id must travel WITH the
file — either as a slugId suffix in the filename (B) or a minimal YAML frontmatter
`id:` (C); both robust, B/C is the open UX decision (author leans C for clean
names). The sidecar may remain an optional path->id cache. Adds phase 6 — link
sync between notes: Docmost links are by pageId (survive rename), vault markdown
links are by path (rewrite on rename, Obsidian-style); independent of B/C and the
format phases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Per owner: test data, no migration. parsePageFile no longer reads the old
docmost:meta block — a file without a gitmost_id frontmatter is simply un-tracked
(adopt). Vaults are a cache: rm -rf on the transition, rebuilt native from
Docmost. Simplifies the format work (no fallback). Doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native-Obsidian structure: a page WITH children now lives at its folder-note
<name>/<name>.md (LostPaul Folder Notes convention) with its children alongside;
a leaf stays <name>.md. Folder-notes claim their canonical path before a
same-named child, so the child (a leaf) is the one disambiguated, never the
folder-note — a folder X/ always contains its own note X.

Format-agnostic and safe in isolation: only the destination PATH changes, the
file content/serialization is untouched, so an existing parent relocates via the
move-by-id path (no delete). The frontmatter format flip (pull+push) is next.

6 new layout unit tests (leaf / parent / nested / child-named-as-parent /
twin-parents / childless). 611 engine tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PULL now serializes each page as the native-Obsidian format (serializePageFile:
a minimal gitmost_id frontmatter + the fixpoint markdown body) instead of the
heavy docmost:meta envelope. title/parent/space are derived (filename / folder /
repo), so only the pageId is persisted. readExisting recovers identity from the
gitmost_id frontmatter (parsePageFile) instead of docmost:meta.

Extracted stabilizePageBody() (the export->import->export fixpoint, no meta) so
the native writer and the legacy serializer share the same deterministic body —
re-pulls of an unchanged page stay byte-identical (loop-guard).

Tests: read-existing fixtures rewritten to gitmost_id; apply-pull asserts the
written text is native frontmatter and carries NO docmost:meta (regression
guard). 611 engine tests green.

NOTE: PUSH still reads docmost:meta — the end-to-end cycle is intentionally NOT
runnable until phase 3 (PUSH reads frontmatter + derives title/parent from path)
lands; no vault is wiped/deployed until then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ghost closed this pull request 2026-06-24 04:48:57 +03:00

Pull request closed

Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#148