feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413) #466
Reference in New Issue
Block a user
Delete Branch "feat/413-markdown-default"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Канонический конвертер (#293/#345/#351) созрел для блочного уровня — делаем markdown форматом по умолчанию для чтения/записи ОДНОГО блока:
getNode/patchNode/insertNode. ProseMirror JSON остаётся опцией «для тонких работ». Новых тулов нет, поверхность не растёт. closes #413Стоит стопкой на #412 (
refactor/412-camelcase-tool-names, PR #463) — база PR указывает на ветку #412, поэтому в диффе только изменения #413. Порядок окна: #411 → #412 → #413 → #415.getNode — markdown по умолчанию + schema-derived фоллбэк
getNode(pageId, nodeId, format='markdown').markdown(дефолт) оборачивает ноду как{type:doc,content:[node]}→convertProseMirrorToMarkdownс дефолтами конвертера, сохраняя comment-якоря включая resolved (это чтение под редактирование, неgetPage).json— сырой сабтри (прежнее поведение). Авто-фолбэк не-топ-левел типов (tableRow/Cell/Header по#<index>) в JSON черезcanBeDocChild=docmostSchema.nodes.doc.contentMatch.matchType(...)— не рукописный список. Полеformatна каждом ответе.patchNode/insertNode — XOR-вход, 1→N сплайс, id-нить, guard
Третий аргумент — XOR
{markdown? | node?}(оба optional в схеме, XOR на рантайме; both/neither → чёткая ошибка). markdown-путь: импорт фрагмента → 1→N сплайс (первый блок наследует id цели, остальные — свежие id, disjoint от страницы) → dryreplaceNodeByIdдля #159-ambiguity ДО мутации → сплайс черезreplaceNodeByIdWithMany(patch) /insertNodesRelative(insert). Соседние блоки byte-identical. GuardfindUnrepresentableTableAttrs: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы /node-JSON (fail-closed, до мутации).Сноски
^[...]во фрагменте — первоклассная сноска (канон-импортёр).importMarkdownFragmentделит блоки отfootnotesList, ремапит id сносок фрагмента в свежие uuid (fn-1 фрагмента не коллизит с fn-1 страницы),mergeFootnoteDefinitionsдобавляет через ту же машинерию, чтоinsertFootnote. Splice, убравший последний referrer существующей сноски, роняет её orphan-определение (как полный ре-импорт) — сходимость к единому канону держится.node-ops
replaceNodeByIdWithMany/insertNodesRelative/reassignCollidingBlockIdsвprosemirror-markdown/node-ops(канон после #414) + barrel.How verified
Прогон в
wt413(симлинк-протокол соблюдён, внешний симлинк восстановлен в дефолт):prosemirror-markdown:tscexit 0;vitest run→ 736 passed (41 файл).mcp:tscexit 0;node --test test/unit/*.test.mjs test/mock/*.test.mjs→ 702 passed, 0 fail.apps/server:tsc --noEmit -p apps/server/tsconfig.json→ чисто (кроме предсуществующего@fastify/compress);jest ai-chat-tools.service shared-tool-specs.contract tool-tiers→ 273 passed.patchNode(markdown)блокаdocsCanonicallyEqualполному импорту (нет «второго канона»); id-нить 1→N; уникальность top-level block-id; XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved);^[...]→хвостовой список+перенумерация; orphan-сноска дропается; guard.Checklist
Третий линк breaking-окна, стоит стопкой на #412 (PR #463) — база этого PR указывает на
refactor/412-camelcase-tool-names, в диффе только #413. Мержить после #463.Что сделал: markdown — дефолт для блочных getNode/patchNode/insertNode; JSON — опция. getNode с schema-derived фоллбэком не-топ-левел типов; patchNode/insertNode XOR-вход {markdown|node} + 1→N сплайс с id-нитью и guard'ом на непредставимые table-attr; сноски через канон-импортёр с ремапом id и merge той же машиной, что insertFootnote.
Проверка на стенде: pmd tsc+vitest 736, mcp tsc+node --test 702/702, server tsc чисто + jest 273. Ключевой тест — сходимость: patchNode(markdown) блок docsCanonicallyEqual полному импорту (нет «второго канона»).
Внутренний цикл: 2 прохода. Первый нашёл 2 дефекта — (1) WARNING: orphan-сноска не подчищалась, когда фрагмент без сносок убирал последний referrer существующей сноски (ранний return в mergeFootnoteDefinitions пропускал canonicalizeFootnotes) — это ровно «второй канон», который PR декларирует невозможным; (2) NIT: block-id чеканились без сверки с занятыми id страницы. Оба починены (hasFootnoteArtifacts-гейт + reassignCollidingBlockIds). Второй проход (адверсарно на дельту) — APPROVE, регрессий/над-канонизации нет.
Ревью — #466
feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)Вердикт: PASS ✅
Полный веер из 9 аспектов по ИЗОЛИРОВАННОЙ дельте #413 (база — ветка #412/#463
refactor/412-camelcase-tool-names, НЕ develop; #463 ещё не влит; ревьюил7cb3199d..e6171a18= 17 файлов, +1888/−119). Третий линк breaking-окна #416. Гейт зелёный.Корректность splice/XOR/converter — проверено (клик)
markdown становится ДЕФОЛТНЫМ форматом для блочных getNode/patchNode/insertNode (PM JSON — опция). Новых тулов нет.
{type:doc,content:[node]}→ convertProseMirrorToMarkdown, сохраняя comment-якоря ВКЛ resolved (read-for-editing, не getPage — подтверждено: без stripComments).format:'json'→ сырой сабтри (прежнее поведение, байт-идентично). Non-top-level (tableRow/Cell/Header по#<index>) АВТО-фолбэк в json черезcanBeDocChild = docmostSchema.nodes.doc.contentMatch.matchType(...)(schema-derived, верный PM-API; reportformat:'json'). Поле format на ответе.{markdown?|node?}: рантайм-XOR (empty-string markdown = отсутствует через trim; both/neither → чёткая ошибка). markdown-путь: importMarkdownFragment → 1→N сплайс in-place (порядок сохранён, соседи byte-identical), первый блок наследует id таргета (skipIndex защищает от re-mint), остальные — свежие id. insertNode position/anchor семантика цела (markdown меняет ЧТО, не ГДЕ).do..while(used.has).docsCanonicallyEqual(блок == тот же markdown импортированный целиком).Regressions/Security/Testcov — проверено
format:'json'байт-идентичен). patchNode/insertNode arg-shape XOR — JSON-путь сохранён (patchNodeJson), tool-specs-обёртка транслирует схему→XOR корректно, старые вызыватели сnodeработают.assertValidNodeShape(#409 findInvalidNode) на каждый блок ДО collab/lock → инъекция типа вне схемы ловится. Resolved-якоря — юзер уже имеет доступ, утечки нет. 1→N сплайс bounded (нет патологического вектора). table-cell с непредставимыми attrs (colspan/rowspan/colwidth/bg) — гард findUnrepresentableTableAttrs.Объективный гейт (клин-рум /home/claude/gm-ci)
⛔ DROP — кодеру НЕ делать (лог калибровки; честно про адверсариал)
[below-threshold]low[test-coverage] адверсариально занейтралилreassignCollidingBlockIds→no-op → uniqueness-тесты (13/14) НЕ покраснели. Т.е. re-mint не форс-тестируется коллизией: уникальность обеспечивается свежими id импортёра (ВОТ load-bearing+tested механизм), а reassignCollidingBlockIds — belt-and-braces на near-impossible fresh-id-коллизию (36^12). Код корректен by inspection (bounded forEach re-mint, skipIndex защищает наследованный id,usedдедупит и внутри фрагмента). Рекомендую тест с форсированной коллизией (мок id-генератора) чтобы запинить гард, но не блок — фича-уникальность гарантирована и покрыта.DO/ESCALATE нет. Splice корректен (in-place/порядок/id), XOR робастен, canBeDocChild верен, footnote-convergence терминирует, markdown валидируется схемой до записи, breaking-flip задокументирован с достижимым json, round-trip когерентен. Гейт зелёный. Approve.
Merge-order: стоит на #463 (#412); мержить после #463 (окно #411→#412→#413→#415). 9 аспектов (stability+coherence, regressions+security+testcov, conv+docs+simp+arch — все LGTM). Splice/XOR/converter/footnote-термин/schema-validation проверены; breaking-flip задокументирован. Dedup-адверсариал не покраснел — honest DROP (uniqueness от fresh-id, re-mint belt-and-braces). Escalate нет.