4be4a75fa322b92699843b3d712466f8836d8bba
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e6171a1810 |
fix(mcp): сходимость orphan-сноски + дедуп block-id в markdown-splice (#413, ревью)
Правки по внутреннему ревью #413. 1. Orphan-сноска (нарушение «no second canon»). mergeFootnoteDefinitions раньше делал ранний return при пустых definitions, пропуская canonicalizeFootnotes. Если markdown-фрагмент БЕЗ сносок заменял/удалял блок, бывший последним referrer'ом существующей сноски, её определение оставалось orphan в хвостовом списке (полный ре-импорт его бы убрал). Теперь fast-path (возврат doc по ссылке без clone) только когда defs.length===0 И !hasFootnoteArtifacts(doc); иначе клон → append (no-op при пустых) → normalizeAndMergeFootnotes → canonicalizeFootnotes, как при полном импорте. Новый предикат hasFootnoteArtifacts обходит дерево на любой footnotesList/footnoteReference (существующие walk/isObject). Идемпотентно: несвязанный plain-патч на странице со сносками не трогает их топологию (canonicalizeFootnotes шаг 6 возвращает как есть). 2. Block-id disjoint от страницы. Новый экспорт reassignCollidingBlockIds (liveDoc, blocks, skipIndex?) в prosemirror-markdown/node-ops (переиспользует collectIds/makeFreshId) + barrel. Зовётся перед сплайсами в client.ts: patch — (liveDoc, threaded, 0) (skip 0 = блок, унаследовавший id цели); insert — (liveDoc, blocks) без skip. used-аккумулятор ловит и коллизию со страницей, и внутрифрагментную. freshBlockId/freshId без изменений. Тесты (+6, markdown-patch-insert): orphan-repro (0 defs/0 list/0 refs + docsCanonicallyEqual полному импорту), fast-path (footnote-free patch не трогает топологию, соседи byte-identical), insert канонизирует при пустых defs со страничной сноской, уникальность top-level block-id для patch 1→N и insert N. mcp node --test 702/702, pmd vitest 736, tsc чисто. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d269bd9efe |
feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)
Канонический конвертер (#293/#345/#351) зрел для блочного уровня: markdown становится дефолтом чтения/записи ОДНОГО блока, PM JSON — опция «для тонких работ». Новых тулов нет, поверхность не растёт. Имена уже camelCase (стоит на #412). - getNode(pageId, nodeId, format='markdown'): дефолт markdown — обёртка {type:doc,content:[node]} → convertProseMirrorToMarkdown, comment-якоря (ВКЛЮЧАЯ resolved) сохранены (это чтение под редактирование, не getPage). format:'json' — сырой сабтри. Авто-фолбэк не-топ-левел типов (tableRow/Cell/ Header по #<index>) в JSON через canBeDocChild = docmostSchema.nodes.doc. contentMatch.matchType (не рукописный список); поле format на каждом ответе. - patchNode/insertNode: XOR-вход {markdown?|node?} (оба optional в схеме, XOR на рантайме). markdown → импорт фрагмента → 1→N сплайс: первый блок наследует id цели, остальные свежие; dry replaceNodeById для #159-ambiguity ДО сплайса; соседние блоки byte-identical. Guard findUnrepresentableTableAttrs: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы/node-JSON. insertNode — insertNodesRelative (N блоков по порядку); голый tableRow/Cell JSON-only. - Сноски: ^[...] во фрагменте → канон-импортёр; importMarkdownFragment делит блоки от footnotesList, РЕМАПИТ id сносок фрагмента в свежие uuid (fn-1 фрагмента не коллизит с fn-1 страницы), mergeFootnoteDefinitions добавляет через ту же машинерию appendDefinition→normalizeAndMergeFootnotes→ canonicalizeFootnotes, что insertFootnote. Сырые JSON-пути не тронуты. - node-ops: replaceNodeByIdWithMany/insertNodesRelative (сплайс массива) в prosemirror-markdown/node-ops (канон после #414) + barrel. - ROUTING_PROSE (READ/EDIT), compile-time client-call contract, CHANGELOG (getNode default→markdown, breaking для внешних клиентов, в окне #411/#412). Тесты: сходимость (patchNode(markdown) блок docsCanonicallyEqual полному импорту — нет «второго канона»); id-нить 1→N (первый наследует, остальные свежие, соседи byte-identical); XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved); ^[...]→хвостовой список+перенумерация; guard. mcp node --test 697/697; pmd vitest 736; tsc чисто; server jest 273. Третий линк breaking-окна, стоит на #412 (#411→#412→ЭТОТ→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7cb3199d09 |
refactor(mcp)!: BREAKING — все имена MCP-тулов snake_case → camelCase (унификация с in-app) (#412)
Один логический тул жил под двумя именами: внешний MCP snake_case (edit_page_text), in-app camelCase (editPageText) — дублирование доков, путаница при переносе промптов/скиллов, помеха шарингу спек (#294). Решение владельца: единый camelCase везде, включая внешний MCP. После этого mcpName === inAppKey. - tool-specs.ts: mcpName ВЫВЕДЕН из ключа спеки (mcpName == inAppKey) для всех 43 shared-спек — раньше divergent snake, теперь равен ключу (проверено: mcpName читается только структурно — цикл регистрации, генератор <tool_inventory>, TOOL_FAMILY). +5 inline-регистраций (tableGet/updateComment/deleteComment/ docmostTransform; search без изменений). Рантайм: 47 тулов, все camelCase, ноль подчёркиваний. - Контракт-конвенция ИНВЕРТИРОВАНА: shared-tool-specs.contract.spec `mcpName === toSnake(inAppKey)` → `mcpName === inAppKey`; tool-specs.test и tool-inventory.test обновлены. - ROUTING_PROSE/TOOL_FAMILY/INLINE_MCP_INVENTORY (server-instructions.ts) → camelCase (105 замен). ai-chat.prompt/guard уже на in-app camelCase-ключах — без изменений (guard прошёл). comment-signal EXCLUDED_TOOLS схлопнут с дублей snake+camel до camelCase. - Некоторое неочевидное: assertUnambiguousMatch(op: "patch_node"|"delete_node") в prosemirror-markdown/node-ops — op интерполируется в model-facing ошибку; литерал-юнион + call-sites → "patchNode"|"deleteNode". - Все snake-имена в описаниях/error-строках/комментах/тестах/доках → camelCase (whole-token, longest-match-first). CHANGELOG: BREAKING-таблица 46 строк + миграция (allowlists mcp__gitmost-*__get_node→__getNode, промпты/скиллы, .mcp.json, метрики по tool-label); релизится вместе с #411. Внутренние имена методов (PageService.updatePageContent и т.п.) НЕ тронуты — переименованы только ИМЕНА ТУЛОВ. Гейт: mcp node --test 677/677; tsc -p apps/server чисто; jest ai-chat-tools. service + shared-tool-specs.contract + tool-tiers + ai-chat.prompt + comment-signal-inapp → 323. Второй линк breaking-окна (#411→ЭТОТ→#413→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0f5f048ca2 |
fix(mcp): pre-validate node JSON против схемы + путь битого узла (#409, остаток Фазы 1)
Структурные редакторы (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>
|
||
|
|
4b2af3d34a |
refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)
Аудит при подготовке #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> |
||
|
|
d6d7dd82f6 |
feat(prosemirror-markdown): new headless converter package seeded from git-sync (#293 stage 1)
Create @docmost/prosemirror-markdown — the single framework-free ProseMirror<-> Markdown converter + schema mirror that git-sync and mcp will both consume, ending the three-hand-synced-copies drift (#293). This step only CREATES the package (no consumer yet; git-sync untouched); the switch of git-sync and mcp onto it, plus the canonical format decisions, come in later commits of this PR. - packages/prosemirror-markdown/src/lib/: the 8 converter-core files copied VERBATIM from packages/git-sync/src/lib (docmost-schema, markdown-converter, markdown-to-prosemirror, canonicalize, markdown-document, node-ops, page-file, index). Confirmed byte-identical — no behavioral drift introduced. - src/index.ts barrel; package.json (@tiptap/* + jsdom/marked/zod, editor-ext workspace devDep for the contract test); tsconfig/vitest configs. - 24 converter-core test files + fixtures copied (engine-coupled layout/ redteam-layout-title tests correctly excluded — they import ../src/engine). - pnpm-lock importer added; build/ gitignored (CI-built). Verified (clean checkout, no network): pnpm --frozen-lockfile EXIT 0; tsc EXIT 0; vitest 23 files, 443 passed | 1 expected-fail (the same image-diagrams known-limitation carried from git-sync) — faithful extraction. git-sync untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |