feat(mcp): детерминированная нормализация текста сносок + слияние по нормализованному тексту — гарантированно склеивать сноски-«форки» (кавы… #419
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Проблема
Сноски с одинаковым смыслом, но разным начертанием символов (типографские кавычки
«…»/"…"vs ASCII"…", em/en-dash vs-, NBSP vs обычный пробел, разное число пробелов) не распознаются как одинаковые и «форкаются» — на странице появляется два определения вместо одного. Наблюдается в проде.Почему текущий дедуп это не ловит:
footnoteContentKey(packages/mcp/src/lib/footnote-authoring.ts:31-50), используется вinsertInlineFootnote(transforms.ts:708-728) иcommentsToFootnotes. Ключ нормализует только пробелы (.replace(/[ \t\r\n]+/g, " ").trim()) и приклеивает сигнатуру inline-марок. Кавычки, тире и NBSP он не трогает → форки по этим символам дают разные ключи и не сливаются.canonicalizeFootnotes(footnote-canonicalize.ts) работает поattrs.id, а не по тексту. Две сноски-форка имеют разные id → он их не объединяет.Решение (согласовано)
Детерминированная серверная нормализация самого текста определений сносок (не только ключа сравнения) на путях записи MCP, с последующим слиянием определений, у которых нормализованный текст совпал.
Согласованные параметры:
"/', em/en-dash →-, NBSP → обычный пробел, схлопывание пробелов, trim.Точки интеграции
Новый чистый модуль, напр.
packages/mcp/src/lib/footnote-normalize-merge.ts, экспортnormalizeAndMergeFootnotes(doc)— pure, deep-clone входа, идемпотентен.Переиспользовать карты символов из
normalizeForMatch(comment-anchor.ts:22-25—DOUBLE_QUOTES/SINGLE_QUOTES/DASHES; маппинг:82-88). СамnormalizeForMatchне подходит напрямую (возвращает{norm, map}для матчинга якорей, ничего не мутирует) — вынести карты символов в общий хелпер и переиспользовать в обоих местах, либо продублировать константы.НЕ трогать
canonicalizeFootnotes— это pure-mirror редакторского канонизатора, привязанный к общему golden-корпусу (packages/editor-ext/src/lib/footnote/footnote-corpus.ts); любое изменение его поведения ломает twin-тест. Новый пасс — строго отдельная функция.Вызвать новый пасс ПЕРЕД
canonicalizeFootnotesна тех же путях записи, где канонизатор уже вызывается (enforcement rule #228 в шапкеfootnote-canonicalize.ts):markdownToProseMirrorCanonical)packages/mcp/src/lib/collaboration.ts:85updatePageJson(raw JSON write)packages/mcp/src/client.ts:1697copyPageContentpackages/mcp/src/client.ts:1961docmost_transformpackages/mcp/src/client.ts:3670insertInlineFootnotepackages/mcp/src/lib/transforms.ts:770Порядок:
normalizeAndMergeFootnotes(doc)→canonicalizeFootnotes(doc). Канонизатор после перевешивания ссылок добьёт порядок / единственный хвостовой список / удаление сирот.Механика пасса
footnoteDefinition(могут быть вложены в callout/blockquote/list — как вcollectDefinitions), нормализовать текст ихtext-нод in place (замена кавычек/тире/NBSP по картам, схлопывание пробелов, trim).marksна нодах сохранять — начертание выжившей сноски не теряем.footnoteContentKey), чтобы визуально разные заметки (bold vs plain при одинаковом тексте) НЕ сливались. Консервативно: сливаем только при точном совпадении ключа.mergeKey → canonicalId(первое определение в порядке документа выигрывает) и производныйdefId → canonicalIdдля дублей.footnoteReference, у дублей переписатьattrs.idнаcanonicalId.Edge cases
"") — НЕ сливать друг с другом (иначе схлопнутся все пустые в одну). Ключ пустой → пропускать, как уже делаетinsertInlineFootnoteприkey === "".text-нод, но структуру не схлопываем.footnoteDefinition. Текст тела документа (вне сносок) НЕ нормализуем: кавычки в обычных абзацах остаются как есть.Открытый вопрос для реализации
Нормализовать текст всех определений безусловно (единообразно, но переписывает кавычки даже в одиночных, ни с чем не сливаемых сносках → больше git-churn) или только в группах, где реально произошло слияние (меньше потерь, но одиночные форки-по-начертанию не выравниваются). Базовый выбор по формулировке задачи («нормализовывать текст», «похуй на кавычки», «гарантированно») — безусловно все; зафиксировать окончательно на ревью.
DoD / тесты
«…»vs"…"→ одна сноска; то же для тире, NBSP, лишних пробелов.canonicalId; дубли удаляются канонизатором (проверить итоговый один хвостовой список и нумерацию).Вне scope
Связанные
canonicalizeFootnotesна write-paths (те же call-site'ы).insertFootnoteдля in-app агента.