refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg) #429

Merged
vvzvlad merged 1 commits from refactor/414-dedup-node-ops into develop 2026-07-10 07:01:08 +03:00
Collaborator

Summary

Дедупликация четырёх конвертер-смежных форков (рефактор; поведение тулов НЕ меняется, −985 строк нетто):

  1. node-ops (самый дрейфоопасный форк): packages/mcp/src/lib/node-ops.ts (~960 строк) ↔ пакетная копия сведены в ОДНУ в @docmost/prosemirror-markdown. Живая mcp-версия — строгое надмножество замороженного #293-seed'а пакета (сверено по git-истории: seed d6d7dd82, mcp получил 3 позднейших коммита — assertUnambiguousMatch/findAnchorTextIndex+strip-fallback; ни одной уникальной пакет-side правки не потеряно). Новая пакет-копия байт-в-байт идентична прежней mcp-копии (diff = 0). Barrel-экспорт полной поверхности; client.ts/page-search.ts/transforms.ts/collaboration.ts импортируют из пакета; тесты переехали.
  2. footnote-lex + footnote-analyze (vestigial legacy [^id]: диагностика): сведены к ОДНОМУ fence-aware предупреждению («reference-style footnotes → use ^[...]», полезно для класса #410); footnote-lex удалён; footnote-analyze ~210→64 строки.
  3. footnote-authoring → примитивы footnoteContentKey/makeFootnoteDefinition/generateFootnoteId перенесены в пакетный footnote.ts (одна реализация конвенции).
  4. parse-node-arg → перенесён в @docmost/prosemirror-markdown (девиация от ТЗ: сервер CommonJS не может импортировать ESM-only @docmost/mcp, но нативно импортирует пакет — #345); обе копии удалены, консьюмеры перенаправлены.

canonicalizeFootnotes / ENFORCEMENT RULE #228 и comment-anchor/json-edit/text-normalize (mcp) НЕ тронуты. API-поверхность node-ops оставлена чистой для #409 (findInvalidNode) и #413 (splice-варианты).

closes #414

Осознанная остаточная дубликация (нит внутреннего ревью, LOW)

node-ops тянет stripInlineMarkdown; пакет не может импортировать @docmost/mcp (цикл), а mcp-версия text-normalize.ts — домен #408. Поэтому в пакет добавлена локальная text-normalize.ts только с этим примитивом (байт-в-байт == mcp-версии; заголовок документирует дубликацию + источник истины). CI-guard'а против дрейфа пока нет — уйдёт при завершении #408. Флажу для решения; если хочешь, добавлю guard-тест паритета.

How verified

Собрано с временным перенаправлением @docmost/* симлинков на этот worktree (кросс-пакетное разрешение — главный риск), симлинки восстановлены после:

  • pnpm --filter @docmost/prosemirror-markdown build + pnpm --filter @docmost/mcp buildtsc чисто (mcp резолвит перенесённый node-ops из пакета — ключевое подтверждение проводки).
  • coder-прогон: пакет vitest 718 tests pass, mcp node --test 484 pass, server parse-node-arg 5/5.
  • Моя доп-проверка: node-ops/node-ops-table/outline/table-ops/parse-node-arg/footnote-analyze/footnote-warnings-import → 83 pass, 0 fail (перенесённый код резолвится из пакета).
  • git diff тестов: только пути импорта (0 правок ассертов), кроме 3 авторизованных footnote-диагностических тестов.

Внутренний цикл: coder → внутреннее ревреreview (byte-identity/superset по git, отсутствие цикла, footnote-редукция fence-aware, parse-node-arg-девиация валидна) → APPROVE WITH SUGGESTIONS (низкий нит про drift-guard).

Checklist

  • ОДНА копия node-ops, barrel-экспорт из пакета, форка в mcp нет
  • поведение тулов не изменилось (byte-identical копия; ассерты тестов не тронуты, кроме footnote-warning)
  • порядок сборки AGENTS.md соблюдён (пакет собирается до mcp)
## Summary Дедупликация четырёх конвертер-смежных форков (рефактор; поведение тулов НЕ меняется, −985 строк нетто): 1. **node-ops** (самый дрейфоопасный форк): `packages/mcp/src/lib/node-ops.ts` (~960 строк) ↔ пакетная копия сведены в ОДНУ в `@docmost/prosemirror-markdown`. Живая mcp-версия — строгое **надмножество** замороженного #293-seed'а пакета (сверено по git-истории: seed `d6d7dd82`, mcp получил 3 позднейших коммита — `assertUnambiguousMatch`/`findAnchorTextIndex`+strip-fallback; ни одной уникальной пакет-side правки не потеряно). Новая пакет-копия **байт-в-байт идентична** прежней mcp-копии (`diff` = 0). Barrel-экспорт полной поверхности; `client.ts`/`page-search.ts`/`transforms.ts`/`collaboration.ts` импортируют из пакета; тесты переехали. 2. **footnote-lex + footnote-analyze** (vestigial legacy `[^id]:` диагностика): сведены к ОДНОМУ fence-aware предупреждению («reference-style footnotes → use `^[...]`», полезно для класса #410); footnote-lex удалён; footnote-analyze ~210→64 строки. 3. **footnote-authoring** → примитивы `footnoteContentKey`/`makeFootnoteDefinition`/`generateFootnoteId` перенесены в пакетный `footnote.ts` (одна реализация конвенции). 4. **parse-node-arg** → перенесён в `@docmost/prosemirror-markdown` (девиация от ТЗ: сервер CommonJS не может импортировать ESM-only `@docmost/mcp`, но нативно импортирует пакет — #345); обе копии удалены, консьюмеры перенаправлены. `canonicalizeFootnotes` / ENFORCEMENT RULE #228 и `comment-anchor`/`json-edit`/`text-normalize` (mcp) НЕ тронуты. API-поверхность node-ops оставлена чистой для #409 (`findInvalidNode`) и #413 (splice-варианты). closes #414 ## Осознанная остаточная дубликация (нит внутреннего ревью, LOW) node-ops тянет `stripInlineMarkdown`; пакет не может импортировать `@docmost/mcp` (цикл), а mcp-версия `text-normalize.ts` — домен #408. Поэтому в пакет добавлена локальная `text-normalize.ts` только с этим примитивом (байт-в-байт == mcp-версии; заголовок документирует дубликацию + источник истины). CI-guard'а против дрейфа пока нет — уйдёт при завершении #408. Флажу для решения; если хочешь, добавлю guard-тест паритета. ## How verified Собрано с временным перенаправлением `@docmost/*` симлинков на этот worktree (кросс-пакетное разрешение — главный риск), симлинки восстановлены после: - `pnpm --filter @docmost/prosemirror-markdown build` + `pnpm --filter @docmost/mcp build` → **tsc чисто** (mcp резолвит перенесённый node-ops из пакета — ключевое подтверждение проводки). - coder-прогон: пакет **vitest 718 tests pass**, mcp `node --test` **484 pass**, server `parse-node-arg` **5/5**. - Моя доп-проверка: node-ops/node-ops-table/outline/table-ops/parse-node-arg/footnote-analyze/footnote-warnings-import → **83 pass, 0 fail** (перенесённый код резолвится из пакета). - `git diff` тестов: только пути импорта (0 правок ассертов), кроме 3 авторизованных footnote-диагностических тестов. Внутренний цикл: coder → внутреннее ревреreview (byte-identity/superset по git, отсутствие цикла, footnote-редукция fence-aware, parse-node-arg-девиация валидна) → APPROVE WITH SUGGESTIONS (низкий нит про drift-guard). ## Checklist - [x] ОДНА копия node-ops, barrel-экспорт из пакета, форка в mcp нет - [x] поведение тулов не изменилось (byte-identical копия; ассерты тестов не тронуты, кроме footnote-warning) - [x] порядок сборки AGENTS.md соблюдён (пакет собирается до mcp)
agent_coder added 1 commit 2026-07-10 01:07:13 +03:00
Аудит при подготовке #413 нашёл дрейфующие дубли между packages/mcp и
packages/prosemirror-markdown. Четыре дедупа (поведение тулов не меняется):

1. node-ops: форк ~960 строк сведён в ОДНУ копию в prosemirror-markdown (живая
   mcp-версия — строгое надмножество замороженного #293-seed'а пакета; сверено по
   git-истории, новая пакет-копия байт-в-байт == прежней mcp-копии). Barrel-экспорт
   полной поверхности; mcp/client.ts/page-search.ts/transforms.ts/collaboration.ts
   импортируют из пакета; тесты переехали. node-ops тянет stripInlineMarkdown ->
   пакет-локальная text-normalize.ts несёт только этот примитив (mcp-версия —
   домен #408; заголовок документирует дубликацию + источник истины).
2. footnote-lex/footnote-analyze (vestigial legacy [^id]: диагностика): сведены к
   одному fence-aware предупреждению 'reference-style footnotes -> use ^[...]'
   (полезно для класса #410); footnote-lex удалён.
3. footnote-authoring -> примитивы (footnoteContentKey/makeFootnoteDefinition/
   generateFootnoteId) перенесены в пакетный footnote.ts, одна реализация конвенции.
4. parse-node-arg -> перенесён в prosemirror-markdown (не mcp: сервер CommonJS не
   импортирует ESM-only @docmost/mcp, но нативно импортирует пакет), обе копии
   удалены, консьюмеры перенаправлены.

canonicalizeFootnotes/ENFORCEMENT RULE #228 и comment-anchor/json-edit/text-normalize
(mcp) не тронуты. API-поверхность node-ops оставлена чистой для #409/#413.

closes #414

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 01:07:13 +03:00
Collaborator

Ревью — #429 refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)

Вердикт: PASS

Полный веер из 9 аспектов по всему диффу PR (merge-base d90c3b8b..4b2af3d3, −985 строк нетто). Гейт — clean-room с чистой пересборкой (/home/claude/gm-ci, чтобы stale build/ не маскировал перенос): пересобрал @docmost/prosemirror-markdown с нуля → vitest 718 passed (39 файлов); собрал mcp (tsc 0) → node --test 485/485; server tsc — 0. Всё зелёное.

Паритет переноса — проверено (клик)

Рефактор выносит дублированные хелперы из packages/mcp/src/lib/* в общий packages/prosemirror-markdown/src/lib/* и переимпортирует; направление зависимостей mcp → prosemirror-markdown, цикла нет.

  • Байт-идентичность перенесённого (сверено параллельно субагентом + мной): node-ops.ts (все экспорты + assertUnambiguousMatch/findAnchorTextIndex), parse-node-arg.ts (тело идентично, комментарий расширён; играет ESM/CJS-мост — сервер нативно тянет pm, не ESM-mcp), footnote-authoring хелперы (footnoteContentKey/makeFootnoteDefinition/generateFootnoteId), stripInlineMarkdowntext-normalize.ts. stripBalancedWrappers намеренно оставлен в mcp (без цикла).
  • FENCE_RE смена группы — НЕ баг: старый /^(\s*)({3,}|~{3,})/читалmatch[2][0]; новый /^\s*({3,}|~{3,})/ читает match[1][0]. Ведущая (\s*)-группа убрана, маркер стал группой 1 — чтение согласовано, toggle открытие/закрытие идентичен. Проверил напрямую.
  • Переимпорты: client.ts, transforms.ts, collaboration.ts (sanitizeForYjs/findUnstorableAttr), server-tools (parseNodeArg) — все на @docmost/prosemirror-markdown, висячих ссылок на удалённые локальные хелперы нет.
  • Покрытие перенесённого не потеряно: node-ops (pm node-ops.test.ts/-extra + mcp интеграция), parse-node-arg, footnote-canonicalize — все тесты на месте/переуказаны на пакет.

Намеренное изменение поведения (не перенос) — footnote-analyze: сужен с analyzeFootnotes() (4 класса диагностик: dangling/duplicate/empty/in-table) до одного advisory hasLegacyFootnoteDefinition(). Обоснование #414: со #293 STEP 5 legacy [^id]: инертен на импорте (остаётся литеральным текстом), поэтому детальные диагностики больше не описывают то, что строит импортёр — одно предупреждение «используй inline ^[body]». Проверил: публичная форма footnoteWarningsField(){ footnoteWarnings?: string[] } СОХРАНЕНА, внешнего потребителя старых диагностик нет, основной кейс (автор пишет [^id]:-определения) по-прежнему предупреждается, документ не меняется, тесты переписаны и зелёные.


DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] low/high [conventions] тип InsertOptions не реэкспортится из barrel prosemirror-markdown/src/lib/index.ts (реэкспортится только функция insertNodeRelative) — сборка зелёная, текущие вызовы выводят тип из инлайн-литерала, потребителю имя типа не нужно; чистая мелочь удобства. Опц.: export type { InsertOptions } from "./node-ops.js";.
  • [by-design] low/med [regressions] суженный footnote-advisory больше НЕ срабатывает на «висячей» ссылке [^id] без строки-определения [^id]: (старый warned про dangling/table-refs). Это осознанное решение #414 (advisory ключится на синтаксис определения); вернуть детекцию ссылок — расширять scope против #414. Отмечено для видимости, не правка.

DO/ESCALATE нет. Паритет переноса подтверждён (байт-идентичность + мутационно-зелёный clean-room гейт), единственное поведенческое изменение (footnote-analyze) — намеренное #414 с сохранённой публичной формой. Approve.

9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Regressions/parity гонялся дольше прочих (line-by-line old-vs-new перенесённых хелперов + FENCE_RE-группа + footnote-сужение проверены отдельно мной). Escalate нет.

<!-- state:review reviewed_head=4b2af3d3 round=1 verdict=approved --> ## Ревью — #429 `refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по всему диффу PR (merge-base `d90c3b8b`..`4b2af3d3`, −985 строк нетто). Гейт — **clean-room с чистой пересборкой** (`/home/claude/gm-ci`, чтобы stale `build/` не маскировал перенос): пересобрал `@docmost/prosemirror-markdown` с нуля → **vitest 718 passed (39 файлов)**; собрал mcp (tsc 0) → **node --test 485/485**; server tsc — **0**. Всё зелёное. <details> <summary>Паритет переноса — проверено (клик)</summary> Рефактор выносит дублированные хелперы из `packages/mcp/src/lib/*` в общий `packages/prosemirror-markdown/src/lib/*` и переимпортирует; направление зависимостей mcp → prosemirror-markdown, цикла нет. - **Байт-идентичность перенесённого** (сверено параллельно субагентом + мной): `node-ops.ts` (все экспорты + `assertUnambiguousMatch`/`findAnchorTextIndex`), `parse-node-arg.ts` (тело идентично, комментарий расширён; играет ESM/CJS-мост — сервер нативно тянет pm, не ESM-mcp), footnote-authoring хелперы (`footnoteContentKey`/`makeFootnoteDefinition`/`generateFootnoteId`), `stripInlineMarkdown` → `text-normalize.ts`. `stripBalancedWrappers` намеренно оставлен в mcp (без цикла). - **FENCE_RE смена группы — НЕ баг**: старый `/^(\s*)(`{3,}|~{3,})/` читал `match[2][0]`; новый `/^\s*(`{3,}|~{3,})/` читает `match[1][0]`. Ведущая `(\s*)`-группа убрана, маркер стал группой 1 — чтение согласовано, toggle открытие/закрытие идентичен. Проверил напрямую. - **Переимпорты**: client.ts, transforms.ts, collaboration.ts (`sanitizeForYjs`/`findUnstorableAttr`), server-tools (`parseNodeArg`) — все на `@docmost/prosemirror-markdown`, висячих ссылок на удалённые локальные хелперы нет. - **Покрытие перенесённого не потеряно**: node-ops (pm `node-ops.test.ts`/`-extra` + mcp интеграция), parse-node-arg, footnote-canonicalize — все тесты на месте/переуказаны на пакет. **Намеренное изменение поведения (не перенос) — `footnote-analyze`**: сужен с `analyzeFootnotes()` (4 класса диагностик: dangling/duplicate/empty/in-table) до одного advisory `hasLegacyFootnoteDefinition()`. Обоснование #414: со #293 STEP 5 legacy `[^id]:` инертен на импорте (остаётся литеральным текстом), поэтому детальные диагностики больше не описывают то, что строит импортёр — одно предупреждение «используй inline `^[body]`». Проверил: публичная форма `footnoteWarningsField()` → `{ footnoteWarnings?: string[] }` СОХРАНЕНА, внешнего потребителя старых диагностик нет, основной кейс (автор пишет `[^id]:`-определения) по-прежнему предупреждается, документ не меняется, тесты переписаны и зелёные. </details> --- <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` `low/high` **[conventions]** тип `InsertOptions` не реэкспортится из barrel `prosemirror-markdown/src/lib/index.ts` (реэкспортится только функция `insertNodeRelative`) — сборка зелёная, текущие вызовы выводят тип из инлайн-литерала, потребителю имя типа не нужно; чистая мелочь удобства. Опц.: `export type { InsertOptions } from "./node-ops.js";`. - `[by-design]` `low/med` **[regressions]** суженный footnote-advisory больше НЕ срабатывает на «висячей» ссылке `[^id]` без строки-определения `[^id]:` (старый warned про dangling/table-refs). Это осознанное решение #414 (advisory ключится на синтаксис определения); вернуть детекцию ссылок — расширять scope против #414. Отмечено для видимости, не правка. </details> DO/ESCALATE нет. Паритет переноса подтверждён (байт-идентичность + мутационно-зелёный clean-room гейт), единственное поведенческое изменение (footnote-analyze) — намеренное #414 с сохранённой публичной формой. Approve. <sub>9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Regressions/parity гонялся дольше прочих (line-by-line old-vs-new перенесённых хелперов + FENCE_RE-группа + footnote-сужение проверены отдельно мной). Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 01:34:18 +03:00
vvzvlad merged commit 144ffb07f5 into develop 2026-07-10 07:01:08 +03:00
Sign in to join this conversation.