fix(mcp): pre-validate node JSON против схемы + путь битого узла (#409) #461
Reference in New Issue
Block a user
Delete Branch "fix/409-invalid-node-validation"
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
Остаток Фазы 1 роадмапа #416 (независим от стопки Фазы 1б). closes #409
Структурные редакторы (
patchNode/insertNode/updatePageJson/transformPage) кидали опаковый Yjs-крах на агентском JSON с вложенным узлом без/с неизвестнымtype:Failed to encode document to Yjs (fromJSON): Unknown node type: undefined— ГЛУБОКО в энкодере, уже ПОСЛЕ открытия collab-сессии; хинт мислейблил это как проблему атрибута. Агент ретраил вслепую (~34 краха в истории диалогов 06-17…07-07).patchNodeпроверялtypeof node.type === "string"только для КОРНЯ — вложенныеcontent-дети не валидировались.findInvalidNode(doc)вprosemirror-markdown/node-ops.ts: DFS поcontent, возвращает{path, summary}первого узла с отсутствующим/не-строковымtypeили типом/маркой вне схемы. Пример:node.content[1].content[0]: missing "type" (keys: text, marks) — did you mean {"type":"text",...}?. Множество имён — изgetSchema(docmostExtensions), ТОГО ЖЕ, из которого энкод-путь строитdocmostSchema→ «известный тип» обходчика ровно то, что приметPMNode.fromJSON/toYdoc.unstorableYjsError:findInvalidNodeПЕРВЫМ (node-shape крах больше не мислейблится как атрибут) →findUnstorableAttr→ generic-фраза последней.assertValidNodeShapefail-fast ДОgetCollabTokenWithReauth/mutatePageContentв 3 точках: collab-сессия не открывается, page-lock не берётся, сообщение детерминировано.tableUpdateCellне тронут (строит абзац из plain text).patch_node/insert_node/update_page_json: каждый узел, включая вложенные, несёт строковыйtypeиз схемы.How verified
Стенд (symlink
@docmost/prosemirror-markdownвременно на пакет ветки для mcp-шагов, восстановлен):pnpm --filter @docmost/prosemirror-markdown build→ чисто; vitest → 726/726.pnpm --filter @docmost/mcp build→ чисто;node --test test/unit/* test/mock/*→ 613/613.collabTokenFetched === false+changed === false(collab не тронут), на happy-path —true/true.findInvalidNode(path-precise, unknown type/mark, все реальные типы приняты, null-safety); precedence (node-shape > attr, fallback жив для undefined-attr).Внутренний цикл: 1 проход (APPROVE WITH SUGGESTIONS). Ревьюер подтвердил schema-fidelity ПРЯМЫМ сравнением обходчика с энкодером на 7 кейсах + перечислением всех 45 узлов/12 марок (ни ложных положительных «валидное→отклонено», ни пропуска краш-типа); fail-fast до collab/lock во всех 3 точках; приоритет диагностики верен. Три замечания — SUGGESTION-уровня (тест перечисления схемы хардкодит один callout — фактическую полноту ревьюер подтвердил отдельно; depth-guard безобиден т.к. энкодер падает раньше ~3000 vs ~8000; пропуск неузловых детей — безопасное направление), оставил форками.
Связано: #407 (персист этих ошибок в историю — независим, вместе превращают слепые Yjs-ретраи в видимые самокорректирующиеся отказы).
Checklist
Структурные редакторы (patchNode/insertNode/updatePageJson/transformPage) кидали опаковый Yjs-крах на агентском JSON с вложенным узлом без/с неизвестным `type`: «Failed to encode document to Yjs (fromJSON): Unknown node type: undefined» — ГЛУБОКО в энкодере, уже ПОСЛЕ открытия collab-сессии, а хинт мислейблил это как проблему атрибута. Агент ретраил вслепую (~34 краха в истории 06-17…07-07). - findInvalidNode(doc) в prosemirror-markdown/node-ops.ts: DFS по content, возвращает {path, summary} первого узла с отсутствующим/не-строковым `type` или типом/маркой вне схемы. Множество имён — из getSchema(docmostExtensions), ТОГО ЖЕ, из которого энкод-путь строит docmostSchema → «известный тип» обходчика ровно то, что примет PMNode.fromJSON/toYdoc (сверено на 45 узлах + 12 марках, ни ложных положительных, ни пропуска краш-типа). - unstorableYjsError: findInvalidNode ПЕРВЫМ (node-shape крах больше не мислейблится как атрибут), затем findUnstorableAttr, generic-фраза последней. - assertValidNodeShape(op, node) ДО getCollabTokenWithReauth/mutatePageContent в patchNode/insertNode/updatePageJson: fail-fast — collab-сессия не открывается, page-lock не берётся, сообщение детерминировано (mock-тест ассертит collabTokenFetched===false на битом пути). tableUpdateCell не тронут (строит абзац из plain text через makeCellParagraph, агентский JSON не глотает). - Описания patch_node/insert_node/update_page_json: каждый узел, включая вложенные, несёт строковый `type` из схемы; текст-листы {"type":"text",...}. sanitizeForYjs (стрип undefined-атрибутов) сохранён — другой класс отказа. Внутреннее ревью: APPROVE WITH SUGGESTIONS — schema-fidelity/fail-fast/ no-false-positive/precedence подтверждены; замечания необязательны (тест перечисления схемы, depth-guard безобиден т.к. энкодер падает раньше). prosemirror-markdown vitest 726/726, mcp node --test 613/613. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — #461
fix(mcp): pre-validate node JSON против схемы + путь битого узла (#409)Вердикт: PASS ✅
Полный веер из 9 аспектов (standalone на develop
f8d37d89; +613/−5). Гейт зелёный, три гарда мутационно-пришпилены.Механизм — schema-fidelity + fail-fast, проверено (клик)
Проблема (#409): структурные редакторы (patch_node/insert_node/update_page_json) кидали ОПАКОВЫЙ Yjs-крах
Unknown node type: undefinedГЛУБОКО в энкодере, УЖЕ ПОСЛЕ открытия collab-сессии, на агентском JSON с вложенным узлом без/с неизвестнымtype(patch_node валидировалtypeтолько у КОРНЯ; вложенные дети — нет → ~34 слепых ретрая в истории).findInvalidNode(doc)(prosemirror-markdown/node-ops.ts): DFS поcontent, первый узел с missing/non-stringtype/ неизвестным типом / неизвестной маркой →{path, summary}(path-anchored, e.g.node.content[2].content[0]: missing "type" …). Schema-fidelity подтверждена у истока: known-set =getSchema(docmostExtensions), а mcpdocmost-schema.tsИМПОРТИРУЕТ тот жеdocmostExtensionsиз пакета и строитdocmostSchema = getSchema(docmostExtensions)→ множество обходчика ТОЧНО = что приметPMNode.fromJSON/toYdoc. Ни ложных положительных (валидное→отклонено), ни пропуска краш-типа.assertValidNodeShapefail-fast в 3 точках (update_page_json:1740, patch_node:2168, insert_node:2263) — ДОgetCollabTokenWithReauth/mutate (и послеensureAuthenticated→ authz не байпасится). Битый ввод: collab-сессия НЕ открывается, page-lock НЕ берётся.unstorableYjsErrorпереупорядочен: findInvalidNode ПЕРВЫМ → findUnstorableAttr → generic (node-shape больше не мислейблится как атрибут; genuine-attr fallback жив).Три адверсариальные мутации (гейт) — все гарды держат:
Объективный гейт (клин-рум /home/claude/gm-ci)
invalid-node-validation.test.mjs— РЕАЛЬНЫЙ Hocuspocus-стек, ассертитcollabTokenFetched===false+changed===falseна битых путях всех 3 тулов,true/trueна happy;unstorable-yjs-error.test.mjs— precedence: node-shape > attr, attr-fallback для genuine undefined-attr).Error, не в prompt/HTML-темплейт → инъекции нет. tool-specs описания — только проза, схемы не тронуты.⛔ DROP — кодеру НЕ делать (лог калибровки)
[by-design]low[coherence] transformPage НЕ fail-fast (его doc из JS-трансформа) — выход РЕЛЕЙБЛИТСЯ через unstorableYjsError, не пре-валидируется. Но опаковости больше нет (цель #409 достигнута), просто не пре-эмптивно. Осознанно (вход трансформа — не сырой node-JSON).[speculative]low[stability] patch_node/insert_node не зовут validateDocStructure(200) перед walk → рекурсия findInvalidNode неограничена. НО: одиночный edit-узел мелок по природе, энкодер (PMNode.fromJSON) УЖЕ рекурсил неограниченно на том же входе до PR (нет НОВОГО риска), а deep-doc путь (update_page_json) ограничен validateDocStructure(200) до walk. Не худший краш, чем исходный.[below-threshold]low[test-coverage] нет ОТДЕЛЬНОГО теста tableCell/tableRow с вложенным битым узлом (DFS их достаёт, покрытие имплицитно). Дёшево добавить, не блокер.DO/ESCALATE нет. Schema-fidelity подтверждена у истока (единый docmostExtensions), fail-fast до collab/lock во всех 3 точках, precedence верна; три гарда мутационно load-bearing, гейт зелёный. Опаковый пост-collab крах превращён в pre-write path-precise ошибку. Approve.
9 аспектов (stability+security+regressions, coherence+testcov, conv+docs+simp+arch — все LGTM). Schema-множество обходчика = энкодерное (единый источник); fail-fast/precedence/edge-cases статически подтверждены; 3 адверсариальные мутации краснеют (nested-recursion/schema-set/fail-fast-placement). Архитектура 3-site guard — верный шов (единственные 3 raw-node-input API + encode-time backstop). Escalate нет.