feat(mcp): буквальная запись markdown (без math/autolink) + getPage format:text (#502) #552

Merged
vvzvlad merged 3 commits from feat/502-mcp-md-modes into develop 2026-07-12 17:35:38 +03:00
Collaborator

Что (#502)

Два machine-friendly режима для MCP-агента над markdown:

1. Агентская запись markdown — буквально

Агент, пишущий markdown, часто НЕ имеет в виду TeX-формулы и автоссылки: $x=1$ в его тексте — это переменная, www.foo.com — просто слово. Расширения канона молча превращали их в mathInline/ссылку.

  • Пакетный импортёр @docmost/prosemirror-markdown параметризован: MarkdownImportOptions {parseMath?=true; fuzzyLinkify?=true}, мемоизированный getMarkedInstance(parseMath, fuzzyLinkify). Оба флага по умолчанию true — дефолтное поведение не меняется.
  • Роутинг по вызывающему (а не в общей обёртке):
    • updatePageMarkdown (агентская запись) и фрагментный patch_node/insert_node → расширения OFF (буквально).
    • import_page_markdownDEFAULTS(ON) — сохраняет lossless export→import (#328): экспортированный $x^2$ реимпортится в math-ноду.
    • Тела комментариев → DEFAULTS.
  • createPage грузит через серверный /pages/import: добавлено опциональное multipart-поле disableMarkdownExtensions (по умолчанию false → human-загрузки файлов сохраняют формулы/автоссылки; MCP createPage шлёт true).

2. Машинное чтение: getPage format:"text"

getPage умеет format:"text" — детерминированный jsonToText (стабильный для diff между версиями), в дополнение к markdown/json.

Как проверял (staging)

  • Рантайм импортёра из сборки: parseMath OFF → $…$ литерал, export A=$FOO без формул; fuzzyLinkify OFF → schemeless www.foo.com/email не линкуются, но явный https://… — линкуется (подавление настоящее, разобрана механика override-токенайзера marked 17).
  • #328 round-trip на РЕАЛЬНОМ пути markdownToProseMirrorCanonical (не на дефолтном пакетном импортёре — это была ловушка ложного грина): math-нода → $x^2$ → import_page_markdown → math-нода. Wiring-тест гоняет через живой Hocuspocus + реальные клиентские методы.
  • createPage end-to-end: клиент шлёт disableMarkdownExtensions=true → контроллер парсит (=== 'true', отсутствие/"false"/не-строка → false, fail-safe) → сервис прокидывает флаги; human-загрузка без поля сохраняет формулы.

внутренний цикл: 2 прохода. Проход 1 нашёл ДВА блокера: (а) createPage вообще не покрывался (грузит через серверный /pages/import с дефолтами → $x=1$ всё равно становился формулой); (б) import_page_markdown ошибочно ВЫКЛючал расширения → ломал #328 lossless round-trip, а тесты давали ложный грин (гоняли дефолтный импортёр, не реальный путь). Проход 2 после разводки флагов по вызывающим + серверного флага + переписанных на реальный путь тестов — чисто.

closes #502

## Что (#502) Два machine-friendly режима для MCP-агента над markdown: ### 1. Агентская запись markdown — буквально Агент, пишущий markdown, часто НЕ имеет в виду TeX-формулы и автоссылки: `$x=1$` в его тексте — это переменная, `www.foo.com` — просто слово. Расширения канона молча превращали их в mathInline/ссылку. - Пакетный импортёр `@docmost/prosemirror-markdown` параметризован: `MarkdownImportOptions {parseMath?=true; fuzzyLinkify?=true}`, мемоизированный `getMarkedInstance(parseMath, fuzzyLinkify)`. Оба флага по умолчанию `true` — дефолтное поведение не меняется. - Роутинг по вызывающему (а не в общей обёртке): - `updatePageMarkdown` (агентская запись) и фрагментный `patch_node`/`insert_node` → расширения **OFF** (буквально). - `import_page_markdown` → **DEFAULTS(ON)** — сохраняет lossless export→import (#328): экспортированный `$x^2$` реимпортится в math-ноду. - Тела комментариев → DEFAULTS. - `createPage` грузит через серверный `/pages/import`: добавлено опциональное multipart-поле `disableMarkdownExtensions` (по умолчанию false → human-загрузки файлов сохраняют формулы/автоссылки; MCP `createPage` шлёт `true`). ### 2. Машинное чтение: `getPage` format:"text" `getPage` умеет `format:"text"` — детерминированный `jsonToText` (стабильный для diff между версиями), в дополнение к markdown/json. ## Как проверял (staging) - Рантайм импортёра из сборки: parseMath OFF → `$…$` литерал, `export A=$FOO` без формул; fuzzyLinkify OFF → schemeless `www.foo.com`/email не линкуются, но явный `https://…` — линкуется (подавление настоящее, разобрана механика override-токенайзера marked 17). - #328 round-trip на РЕАЛЬНОМ пути `markdownToProseMirrorCanonical` (не на дефолтном пакетном импортёре — это была ловушка ложного грина): math-нода → `$x^2$` → import_page_markdown → math-нода. Wiring-тест гоняет через живой Hocuspocus + реальные клиентские методы. - createPage end-to-end: клиент шлёт `disableMarkdownExtensions=true` → контроллер парсит (`=== 'true'`, отсутствие/`"false"`/не-строка → false, fail-safe) → сервис прокидывает флаги; human-загрузка без поля сохраняет формулы. внутренний цикл: 2 прохода. Проход 1 нашёл ДВА блокера: (а) `createPage` вообще не покрывался (грузит через серверный /pages/import с дефолтами → `$x=1$` всё равно становился формулой); (б) `import_page_markdown` ошибочно ВЫКЛючал расширения → ломал #328 lossless round-trip, а тесты давали ложный грин (гоняли дефолтный импортёр, не реальный путь). Проход 2 после разводки флагов по вызывающим + серверного флага + переписанных на реальный путь тестов — чисто. closes #502
agent_coder added 2 commits 2026-07-12 07:16:05 +03:00
WRITE: parameterize the canonical importer markdownToProseMirror with
{parseMath, fuzzyLinkify} (defaults true — editor/file-import/git-sync unchanged);
MCP write paths (createPage/updatePageMarkdown/patchNode/insertNode) call with both
false so $...$ stays literal text and bare domains don't autolink (explicit
https:// still links). READ: getPage format:'text' reuses the server jsonToText path
(deterministic flag) for flat machine-diffable text, [image]/[table RxC] placeholders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Internal review found the two layered-extension flags landed in the SHARED
markdownToProseMirrorCanonical wrapper, which mis-covered two tools. Move the
decision to each caller by the tool's semantics.

BLOCKER 1 — createPage was NOT covered. createPage POSTs to the server
/pages/import endpoint, which imported with DEFAULTS (math + fuzzy autolink ON),
so an agent's `$x=1$` still became a formula and `www.host` still autolinked.
Add an optional `disableMarkdownExtensions` multipart field to /pages/import
(default OFF, so human file uploads keep math ON); import.controller reads it,
import.service.importPage/processMarkdown thread it into markdownToProseMirror.
MCP createPage sends it true.

BLOCKER 2 — import_page_markdown was wrongly disabled. It goes through the same
wrapper, so hardcoding parseMath:false degraded an exported `$x^2$` to literal
text on re-import, breaking the #328 lossless export→import pair. The wrapper no
longer hardcodes the flags: it takes them from the caller and DEFAULTS to the
package importer defaults (extensions ON). Callers now set them explicitly:
  - updatePageMarkdown (updatePageContentRealtime) -> OFF
  - patch_node/insert_node (importMarkdownFragment) -> OFF (unchanged)
  - import_page_markdown -> DEFAULTS (math ON) — #328 round-trip restored

Tests: rewrite the mcp write test around the corrected per-caller semantics
(agent-write OFF, import_page_markdown DEFAULTS incl. the REAL #328 round-trip);
add a collab-backed wiring test driving client.updatePage (OFF) and
client.importPageMarkdown (DEFAULTS) end-to-end; add a createPage multipart-flag
test; add a server import.service.processMarkdown spec (flag OFF -> literal / no
autolink, default -> math ON for human uploads); reword the prosemirror-markdown
round-trip test to its package-default scope. Mutation-verified both caller
wirings (flip updatePageMarkdown -> defaults reddens the OFF wiring test; make
the wrapper default OFF reddens the #328 round-trip).

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

Ревью — MCP literal-markdown-write + getPage format:text (#502). Вердикт: changes-requested

Крупная фича (22 файла), корректно спроектирована и реализована. Роутинг-по-вызывающему (сердце фичи) сверил ЛИЧНО + 3 агента — mis-route'ов НЕТ; #328 lossless цел; память/дефолты/флаг/format:text — всё держится; гейт полностью зелёный (pm-markdown 790, mcp 879, server tsc 0 + 17). Одна documentation-находка. Эскалаций нет.

Сверенный роутинг (каждый call-site): updatePageMarkdown (collaboration.ts:384) OFF · patch_node/insert_node (markdown-fragment.ts:145) OFF · createPage→server-флаг OFF · import_page_markdown (pages.ts:397, без опций) DEFAULTS(ON) · comment bodies DEFAULTS · human-upload (флаг опущен) DEFAULTS. #328: $x^2$ PM→md→реимпорт → mathInline, byte-stable — верифицировано end-to-end. Memo keyed по обоим флагам (≤4, без cross-contamination/state-leak). Флаг === 'true' (garbage/missing→false, human-загрузки не трогает). format:text детерминирован (document-order, guarded), не течёт больше markdown, после permission-check. Тесты не вакуумны (флип роута краснит; #328-гард пиннит реального вызывающего).

Открыто:

  • F1 — описания 4 write-тулов не обновлены под новое literal-поведение (агент-facing контракт рассинхронизирован с поведением).
📋 F1 детали

F1 [warning · documentation] write-тулы не сообщают агенту про literal-markdown — packages/mcp/src/tool-specs.ts (updatePageMarkdown ~1353, createPage ~1095, patchNode ~458/468, insertNode ~526)

Ядро PR: агентские markdown-записи теперь импортятся с math + fuzzy-autolink OFF. Но обновлён только getPage-spec (~913). Четыре write-тула, чья семантика РЕАЛЬНО поменялась, всё ещё описывают старое поведение по умолчанию: описания не упоминают, что $…$ больше не парсится в формулу, а schemeless www.host не линкуется. Docmost поддерживает $…$-math, поэтому агент резонно напишет $x^2$ через updatePageMarkdown/createPage, ожидая формулу, и молча получит литеральный текст — и в контракте тула НИЧЕГО не говорит, что для настоящей math надо updatePageJson (mathInline/mathBlock). Поведение изменилось, агент-facing описание — нет. Это ломает юзабилити самой фичи для её единственного потребителя (LLM-агента, читающего tool-specs).
Fix: добавить по одному предложению в description updatePageMarkdown/createPage/patchNode/insertNode: $…$/$$…$$ остаются ЛИТЕРАЛЬНЫМИ и schemeless www./email НЕ автолинкуются (явный https://… линкуется), а настоящие формулы писать через page-JSON (updatePageJson, mathInline/mathBlock). Опц. упомянуть format:"text" в packages/mcp/README(.ru).md.

9 аспектов (3 агента + мой разбор роутинг-крюкса). Routing/memoization/security/regressions/stability/coherence/conventions/simplification/architecture/test-coverage — LGTM. Гейт: pm-markdown 790/790, mcp node:test 879/879 (incl 4 новых .mjs, live-Hocuspocus wiring-тест пиннит роут), server tsc 0 + 3 suite/17. Дерево чисто. кодеру НЕ делать (pre-existing, below-threshold): мёртвые импорты markdownToProseMirror/Canonical в tables.ts/media.ts/transforms.ts/doc-validate.ts/nodes-write.ts — не #552.

## Ревью — MCP literal-markdown-write + getPage format:text (#502). Вердикт: **changes-requested** Крупная фича (22 файла), **корректно спроектирована и реализована**. Роутинг-по-вызывающему (сердце фичи) сверил ЛИЧНО + 3 агента — mis-route'ов НЕТ; #328 lossless цел; память/дефолты/флаг/format:text — всё держится; гейт полностью зелёный (pm-markdown 790, mcp 879, server tsc 0 + 17). Одна documentation-находка. Эскалаций нет. **Сверенный роутинг (каждый call-site):** `updatePageMarkdown` (collaboration.ts:384) OFF · `patch_node`/`insert_node` (markdown-fragment.ts:145) OFF · `createPage`→server-флаг OFF · `import_page_markdown` (pages.ts:397, без опций) DEFAULTS(ON) · comment bodies DEFAULTS · human-upload (флаг опущен) DEFAULTS. #328: `$x^2$` PM→md→реимпорт → mathInline, byte-stable — верифицировано end-to-end. Memo keyed по обоим флагам (≤4, без cross-contamination/state-leak). Флаг `=== 'true'` (garbage/missing→false, human-загрузки не трогает). format:text детерминирован (document-order, guarded), не течёт больше markdown, после permission-check. Тесты не вакуумны (флип роута краснит; #328-гард пиннит реального вызывающего). Открыто: - **F1 — описания 4 write-тулов не обновлены под новое literal-поведение** (агент-facing контракт рассинхронизирован с поведением). <details> <summary>📋 F1 детали</summary> ### F1 [warning · documentation] write-тулы не сообщают агенту про literal-markdown — `packages/mcp/src/tool-specs.ts` (updatePageMarkdown ~1353, createPage ~1095, patchNode ~458/468, insertNode ~526) Ядро PR: агентские markdown-записи теперь импортятся с math + fuzzy-autolink OFF. Но обновлён только `getPage`-spec (~913). Четыре write-тула, чья семантика РЕАЛЬНО поменялась, всё ещё описывают старое поведение по умолчанию: описания не упоминают, что `$…$` больше не парсится в формулу, а schemeless `www.host` не линкуется. Docmost поддерживает `$…$`-math, поэтому агент резонно напишет `$x^2$` через `updatePageMarkdown`/`createPage`, ожидая формулу, и молча получит литеральный текст — и в контракте тула НИЧЕГО не говорит, что для настоящей math надо `updatePageJson` (`mathInline`/`mathBlock`). Поведение изменилось, агент-facing описание — нет. Это ломает юзабилити самой фичи для её единственного потребителя (LLM-агента, читающего tool-specs). **Fix:** добавить по одному предложению в `description` `updatePageMarkdown`/`createPage`/`patchNode`/`insertNode`: `$…$`/`$$…$$` остаются ЛИТЕРАЛЬНЫМИ и schemeless `www.`/email НЕ автолинкуются (явный `https://…` линкуется), а настоящие формулы писать через page-JSON (`updatePageJson`, `mathInline`/`mathBlock`). Опц. упомянуть `format:"text"` в packages/mcp/README(.ru).md. <sub>9 аспектов (3 агента + мой разбор роутинг-крюкса). Routing/memoization/security/regressions/stability/coherence/conventions/simplification/architecture/test-coverage — LGTM. Гейт: pm-markdown 790/790, mcp node:test 879/879 (incl 4 новых .mjs, live-Hocuspocus wiring-тест пиннит роут), server tsc 0 + 3 suite/17. Дерево чисто. ⛔ кодеру НЕ делать (pre-existing, below-threshold): мёртвые импорты `markdownToProseMirror`/Canonical в tables.ts/media.ts/transforms.ts/doc-validate.ts/nodes-write.ts — не #552.</sub> </details>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 07:51:57 +03:00
agent_coder added 1 commit 2026-07-12 08:02:40 +03:00
The four MCP markdown-write tools (updatePageMarkdown/createPage/patchNode/
insertNode) still described the old parse-everything behavior after #502 turned
TeX math and fuzzy autolink OFF on the write paths. Add a concise contract hint
to each: $...$ / $$...$$ stay literal (not a math formula) and schemeless
www.host / bare emails are not auto-linked (explicit https:// still links); for
a real formula use updatePageJson (or a mathInline/mathBlock node via `node` on
patch/insert). Also note getPage format:"text" in README.md/README.ru.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Ответ на ревью (F1)

F1 [warning · documentation] — write-тулы не сообщали агенту про literal-markdown — исправлено.

Добавил по предложению в description четырёх write-тулов (packages/mcp/src/tool-specs.ts), синхронизировав agent-facing контракт с реальным поведением #502:

  • updatePageMarkdown, createPage: «Text is taken LITERALLY — $...$/$$...$$ are NOT parsed into a math formula and schemeless www.host / bare emails are NOT auto-linked (an explicit https:// URL still links); for a real formula use updatePageJson with mathInline/mathBlock nodes instead.»
  • patchNode, insertNode (в буллете markdown): то же про литеральность + указатель на in-tool escape-hatch — «pass a mathInline/mathBlock ProseMirror node via node (or updatePageJson)» (для этих тулов raw-node естественнее, чем markdown-путь).

getPage-спека (уже была обновлена) не тронута. Плюс одна строка про format:"text" в README.md и README.ru.md (flat deterministic plain-text, плейсхолдеры [image]/[table RxC], для machine-diff).

Верификация: @docmost/mcp пересобран (tsc 0, registry-stamp регенерён — gitignored, не в коммите); mcp-сьют 879/879, ни один тест не ассертит текст описаний. Скоуп коммита: tool-specs.ts + два README, 0 node_modules. Ветка на уровне develop (0 позади).

внутренний цикл: 1 проход — проверка фактической точности каждого из 4 добавленных утверждений против реального роутинга: все истинны (каждый тул реально импортит с {parseMath:false, fuzzyLinkify:false}; явный https:// действительно линкуется — noFuzzyUrlTokenizer дропает только schemeless/email; updatePageJson/mathInline/mathBlock — валидные имена; raw-node реально принимается). Ложных обещаний агенту нет.

Новый head: ed3a8f81.

## Ответ на ревью (F1) **F1 [warning · documentation] — write-тулы не сообщали агенту про literal-markdown — исправлено.** Добавил по предложению в `description` четырёх write-тулов (`packages/mcp/src/tool-specs.ts`), синхронизировав agent-facing контракт с реальным поведением #502: - **updatePageMarkdown**, **createPage**: «Text is taken LITERALLY — `$...$`/`$$...$$` are NOT parsed into a math formula and schemeless `www.host` / bare emails are NOT auto-linked (an explicit `https://` URL still links); for a real formula use updatePageJson with `mathInline`/`mathBlock` nodes instead.» - **patchNode**, **insertNode** (в буллете `markdown`): то же про литеральность + указатель на in-tool escape-hatch — «pass a `mathInline`/`mathBlock` ProseMirror node via `node` (or updatePageJson)» (для этих тулов raw-`node` естественнее, чем markdown-путь). `getPage`-спека (уже была обновлена) не тронута. Плюс одна строка про `format:"text"` в `README.md` и `README.ru.md` (flat deterministic plain-text, плейсхолдеры `[image]`/`[table RxC]`, для machine-diff). **Верификация:** `@docmost/mcp` пересобран (tsc 0, registry-stamp регенерён — gitignored, не в коммите); mcp-сьют **879/879**, ни один тест не ассертит текст описаний. Скоуп коммита: `tool-specs.ts` + два README, 0 node_modules. Ветка на уровне develop (0 позади). **внутренний цикл:** 1 проход — проверка фактической точности каждого из 4 добавленных утверждений против реального роутинга: все истинны (каждый тул реально импортит с `{parseMath:false, fuzzyLinkify:false}`; явный `https://` действительно линкуется — `noFuzzyUrlTokenizer` дропает только schemeless/email; `updatePageJson`/`mathInline`/`mathBlock` — валидные имена; raw-`node` реально принимается). Ложных обещаний агенту нет. Новый head: `ed3a8f81`.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-12 08:03:06 +03:00
Collaborator

Ре-ревью — MCP literal-markdown + getPage format:text (#502), раунд 2. Вердикт: approved

F1 (write-тулы не сообщали агенту про literal-поведение) закрыт: добавлена нота во ВСЕ 4 write-тула — updatePageMarkdown (:1362), createPage (:1100), patchNode (:459/473), insertNode (:531/542): «$...$/$$...$$ НЕ парсятся в формулу, schemeless www.host/email НЕ автолинкуются (явный https:// линкуется); для настоящей формулы — mathInline/mathBlock через node/updatePageJson». Agent-facing контракт синхронизирован с реальным поведением #502.

Гейт: mcp node:test 879/879. Роутинг/memoization/#328/security/format:text — были верны и в round 1, доки-правка их не трогает. Готово к мержу.

## Ре-ревью — MCP literal-markdown + getPage format:text (#502), раунд 2. Вердикт: **approved** ✅ F1 (write-тулы не сообщали агенту про literal-поведение) закрыт: добавлена нота во ВСЕ 4 write-тула — `updatePageMarkdown` (:1362), `createPage` (:1100), `patchNode` (:459/473), `insertNode` (:531/542): «`$...$`/`$$...$$` НЕ парсятся в формулу, schemeless `www.host`/email НЕ автолинкуются (явный `https://` линкуется); для настоящей формулы — `mathInline`/`mathBlock` через `node`/`updatePageJson`». Agent-facing контракт синхронизирован с реальным поведением #502. **Гейт:** mcp node:test **879/879**. Роутинг/memoization/#328/security/format:text — были верны и в round 1, доки-правка их не трогает. Готово к мержу.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 08:16:06 +03:00
vvzvlad merged commit 32c9361969 into develop 2026-07-12 17:35:38 +03:00
Sign in to join this conversation.