[refactor][git-sync] Три копии схемы/конвертера (editor-ext / mcp / git-sync) — устранить дрейф, порождающий потерю данных #293

Open
opened 2026-07-02 16:41:49 +03:00 by vvzvlad · 0 comments
Owner

Проблема

Схема документа и конвертер ProseMirror ↔ Markdown существуют в проекте в трёх рукописно синхронизируемых копиях:

Копия Назначение Размер (пример)
packages/editor-ext канонические Tiptap-расширения (редактор) source of truth
packages/mcp/src/lib конвертер для standalone-MCP docmost-schema.ts ~1287, markdown-converter.ts ~903
packages/git-sync/src/lib конвертер для git-синка docmost-schema.ts ~1532, markdown-converter.ts ~1065

mcp и git-sync — это разошедшиеся форки одного и того же конвертера, оба вручную зеркалят каноническую схему из editor-ext. Любое изменение схемы редактора требует трёх синхронных правок. Когда одна из копий отстаёт — данные молча теряются на каждом синке.

Почему это важно (а не «просто дубль кода»)

Это уже сработавший и повторяющийся класс потери данных. История только этого одного эпика (#119):

  • 7abce935 — потеря spoiler-марка (#259) и подписи картинки (#221): git-sync-копия отстала от editor-ext;
  • 32e99c6e — потеря атрибута open у details;
  • fe4adf23 — схлопывание типа callout.

И ещё два свежих бага того же класса, найденных при ревью PR #119 (подтверждены живым round-trip repro):

  • выравнивание абзацев экспортируется как <div align="center">, но не парсится обратно → теряется первым же stabilize-проходом (packages/git-sync/src/lib/docmost-schema.ts:236-243);
  • таблицы внутри columns и multi-block ячейки деградируют в литеральный текст (packages/git-sync/src/lib/markdown-converter.ts:963-982, :443-454).

Почему текущие гейты не ловят дрейф

  • packages/git-sync/test/schema-editor-ext-contract.test.ts — сверка только на уровне имён узлов: новый атрибут существующего узла (ровно класс image-caption / alignment) проходит мимо;
  • packages/git-sync/test/schema-surface-snapshot.test.ts — пиннит копию git-sync саму против себя, не против editor-ext;
  • apps/server/src/collaboration/git-sync-converter-gate.spec.ts — рукописный корпус фикстур: новая поверхность схемы проверяется, только если кто-то не забыл добавить фикстуру;
  • у mcp-копии вообще нет механической связи с editor-ext (об этом прямо сказано в комментарии контракт-теста — отложено).

Историческая причина зеркала git-sync (внешний вендоринг из docmost-sync) уже устранена интернализацией движка (коммит 5da12e89): тесты git-sync резолвят @docmost/editor-ext уже сегодня (workspace-зависимость + shamefully-hoist).

Варианты решения

Вариант A — оставить копии, но сделать гейт механическим на уровне атрибутов (effort: S)

Строить реальную схему из расширений editor-ext (через getSchema над тем же набором, что использует apps/server/src/collaboration/collaboration.util.ts) и ассертить попарное совпадение ключей атрибутов каждого узла/марка против каждого зеркала, с закоммиченным allowlist осознанных расхождений. Добавить идентичный тест в packages/mcp.

  • Плюсы: дёшево и сразу; ловит класс image-caption/alignment в CI, а не в проде.
  • Минусы: три копии редактировать всё равно на каждое изменение схемы; поведенческий дрейф parseHTML/renderHTML (баг details open) не покрывается сравнением имён/атрибутов.

Вариант B — вынести единый headless-конвертер, потребляемый git-sync и mcp (effort: L)

Один пакет (@docmost/prosemirror-markdown или подобн.) с зеркалом схемы + конвертером md↔ProseMirror, контракт-тестируемый против editor-ext. Изменение схемы становится двумя местами с механическим контрактом между ними вместо трёх ручных зеркал. Это и есть «единое framework-free ядро схемы», уже упомянутое как отложенный план в комментариях тестов и AGENTS.md.

  • Плюсы: одна копия зеркала + конвертера; убирает целый класс багов.
  • Минусы: большой объём: у двух форков есть реальные расхождения для сведения (алиасинг callout, footnote-пайплайн mcp, разная обработка envelope), риск регрессий в обоих потребителях. Отдельная работа, не в рамках #119.

Вариант C — git-sync импортирует схему напрямую из editor-ext; mcp оставляет зеркало (effort: M)

  • Плюсы: убивает одно из двух зеркал там, где точность синка важнее всего; технически возможно уже сегодня.
  • Минусы: расширения editor-ext тащат редакторный багаж (node views, suggestion-плагины), которого минимальное зеркало намеренно избегает → editor-ext нужен curated headless entry point; mcp-зеркало остаётся, владение схемой/сериализатором размазывается по пакетам.

Рекомендация

Вариант A сейчас — маленький follow-up, оправданный собственным баг-паттерном этого эпика: механический attribute-level контракт закрывает большинство дрейфа в CI немедленно.

Вариант B — как заявленная цель: он уже наполовину обещан в комментариях кода и AGENTS.md; превратить отложенность из комментария в трекаемую задачу, чтобы не гнила.

Вариант C — полушаг, который я бы пропустил в пользу B.

Связанные

  • #294 — дублирование определений инструментов (in-app vs standalone MCP). Обе темы выделены из ранее общего #193 (тот удалён).

Выделено из ревью PR #119. Форвард-луки, мержу #119 не блокирует — но это самый высоколивереджный структурный долг, который фича оставляет за собой.

## Проблема Схема документа и конвертер **ProseMirror ↔ Markdown** существуют в проекте в **трёх рукописно синхронизируемых копиях**: | Копия | Назначение | Размер (пример) | | --- | --- | --- | | `packages/editor-ext` | канонические Tiptap-расширения (редактор) | source of truth | | `packages/mcp/src/lib` | конвертер для standalone-MCP | `docmost-schema.ts` ~1287, `markdown-converter.ts` ~903 | | `packages/git-sync/src/lib` | конвертер для git-синка | `docmost-schema.ts` ~1532, `markdown-converter.ts` ~1065 | `mcp` и `git-sync` — это разошедшиеся форки одного и того же конвертера, оба вручную зеркалят каноническую схему из `editor-ext`. **Любое изменение схемы редактора требует трёх синхронных правок.** Когда одна из копий отстаёт — данные молча теряются на каждом синке. ## Почему это важно (а не «просто дубль кода») Это **уже сработавший и повторяющийся** класс потери данных. История только этого одного эпика (#119): - `7abce935` — потеря spoiler-марка (#259) и подписи картинки (#221): git-sync-копия отстала от editor-ext; - `32e99c6e` — потеря атрибута `open` у details; - `fe4adf23` — схлопывание типа callout. И **ещё два свежих бага того же класса**, найденных при ревью PR #119 (подтверждены живым round-trip repro): - выравнивание абзацев экспортируется как `<div align="center">`, но не парсится обратно → теряется первым же stabilize-проходом (`packages/git-sync/src/lib/docmost-schema.ts:236-243`); - таблицы внутри `columns` и multi-block ячейки деградируют в литеральный текст (`packages/git-sync/src/lib/markdown-converter.ts:963-982`, `:443-454`). ## Почему текущие гейты не ловят дрейф - `packages/git-sync/test/schema-editor-ext-contract.test.ts` — сверка **только на уровне имён узлов**: новый *атрибут* существующего узла (ровно класс image-caption / alignment) проходит мимо; - `packages/git-sync/test/schema-surface-snapshot.test.ts` — пиннит копию git-sync **саму против себя**, не против editor-ext; - `apps/server/src/collaboration/git-sync-converter-gate.spec.ts` — рукописный корпус фикстур: новая поверхность схемы проверяется, только если кто-то не забыл добавить фикстуру; - у `mcp`-копии **вообще нет** механической связи с editor-ext (об этом прямо сказано в комментарии контракт-теста — отложено). Историческая причина зеркала git-sync (внешний вендоринг из `docmost-sync`) **уже устранена** интернализацией движка (коммит `5da12e89`): тесты git-sync резолвят `@docmost/editor-ext` уже сегодня (workspace-зависимость + shamefully-hoist). ## Варианты решения ### Вариант A — оставить копии, но сделать гейт механическим на уровне атрибутов (effort: S) Строить реальную схему из расширений `editor-ext` (через `getSchema` над тем же набором, что использует `apps/server/src/collaboration/collaboration.util.ts`) и ассертить **попарное совпадение ключей атрибутов** каждого узла/марка против каждого зеркала, с закоммиченным allowlist осознанных расхождений. Добавить идентичный тест в `packages/mcp`. - **Плюсы:** дёшево и сразу; ловит класс image-caption/alignment в CI, а не в проде. - **Минусы:** три копии редактировать всё равно на каждое изменение схемы; поведенческий дрейф `parseHTML`/`renderHTML` (баг details `open`) не покрывается сравнением имён/атрибутов. ### Вариант B — вынести единый headless-конвертер, потребляемый git-sync и mcp (effort: L) Один пакет (`@docmost/prosemirror-markdown` или подобн.) с зеркалом схемы + конвертером md↔ProseMirror, контракт-тестируемый против editor-ext. Изменение схемы становится **двумя** местами с механическим контрактом между ними вместо трёх ручных зеркал. Это и есть «единое framework-free ядро схемы», уже упомянутое как отложенный план в комментариях тестов и AGENTS.md. - **Плюсы:** одна копия зеркала + конвертера; убирает целый класс багов. - **Минусы:** большой объём: у двух форков есть реальные расхождения для сведения (алиасинг callout, footnote-пайплайн mcp, разная обработка envelope), риск регрессий в обоих потребителях. Отдельная работа, не в рамках #119. ### Вариант C — git-sync импортирует схему напрямую из editor-ext; mcp оставляет зеркало (effort: M) - **Плюсы:** убивает одно из двух зеркал там, где точность синка важнее всего; технически возможно уже сегодня. - **Минусы:** расширения editor-ext тащат редакторный багаж (node views, suggestion-плагины), которого минимальное зеркало намеренно избегает → editor-ext нужен curated headless entry point; mcp-зеркало остаётся, владение схемой/сериализатором размазывается по пакетам. ## Рекомендация **Вариант A сейчас** — маленький follow-up, оправданный собственным баг-паттерном этого эпика: механический attribute-level контракт закрывает большинство дрейфа в CI немедленно. **Вариант B — как заявленная цель**: он уже наполовину обещан в комментариях кода и AGENTS.md; превратить отложенность из комментария в трекаемую задачу, чтобы не гнила. Вариант C — полушаг, который я бы пропустил в пользу B. ## Связанные - **#294** — дублирование определений инструментов (in-app vs standalone MCP). Обе темы выделены из ранее общего #193 (тот удалён). --- *Выделено из ревью PR #119. Форвард-луки, мержу #119 не блокирует — но это самый высоколивереджный структурный долг, который фича оставляет за собой.*
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#293