[feature][footnotes] Reuse сносок (много ссылок → одна сноска) + предупреждения о пустых/битых сносках при импорте #166
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?
Контекст
При генерации статьи агентом через
create_pageсноски ([^id]) получились битыми: внизу страницы появилось ~70 пустых определений, а повторно использованные ярлыки превратились вkowiki__2 … kowiki__14. Разбор показал, что это не баг конвейера, а сочетание модели «строго 1:1» и ошибок авторской разметки. Эта задача меняет модель так, как «привыкли агенты» (Pandoc-семантика), и добавляет диагностику.Как работает сейчас (по коду)
Импорт Markdown → ProseMirror. Определения сносок собираются только из строк
[^id]: текст:extractFootnotes()—packages/mcp/src/lib/collaboration.ts:380extractFootnoteDefinitions()—packages/editor-ext/src/lib/markdown/utils/footnote.marked.ts:66Inline-токенайзер превращает любой
[^id](в т.ч. в ячейках таблиц) вfootnoteReference.Интеграционный проход редактора
footnoteSyncPlugin/resolveCollisions—packages/editor-ext/src/lib/footnote/footnote-sync.ts:119:[^a]→a__2,a__3черезderiveFootnoteId()(footnote-util.ts:70);footnote-sync.ts:449-451).Нумерация уже поддерживает reuse:
computeFootnoteNumbers()(footnote-util.ts:121) дедуплицирует по id, аbuildFootnoteNumberingState()(footnote-numbering.ts:51) вешает один номер на все ссылки с этим id и на определение. Единственное, что мешает reuse — переименование дублей вresolveCollisions.Важно: разные пути конверсии
create_page(client.ts:972) грузит сырой Markdown на серверный/pages/import→ конверсия и резолв коллизий идут на сервере (сервер использует тот жеFootnoteReferenceиз editor-ext —apps/server/src/collaboration/collaboration.util.ts:121).update_page/import_page_markdown/ комментарии идут через MCPmarkdownToProseMirror()(collaboration.ts:452).Вывод: логика reuse живёт в общем модуле
editor-ext/footnote(его используют клиент, сервер и MCP-зеркало), а предупреждения считаются отдельным текстовым сканом в MCP — он не зависит от пути конверсии и работает одинаково для всех инструментов.Цели
[^id]ссылаются на одну сноску (как в Pandoc) — один номер, одно определение, без__N-пустышек.Зафиксированные дизайн-решения
[^a]: …): первое выигрывает, остальные отбрасываются + предупреждение (на агентском пути импорта). В живом редакторе сохраняем защитный принцип «никогда не терять данные молча».[^…]в таблицах.Реализация
Фича 1 — Reuse (ядро)
packages/editor-ext/src/lib/footnote/footnote-sync.ts→resolveCollisions()referenceIds= различные id ссылок в порядке первого появления; убрать ветку, добавляющуюrefReidsдля повторов ссылок;seenRefIdsлишь дедуплицирует.deriveFootnoteId, оно становится сиротой по существующей orphan-политике). Это путь paste/collab, не импорт.deriveFootnoteIdи его golden-table тест (footnote-util.derive-id.test.ts) остаются — нужны для дубль-определений и paste-плагина.footnotesList, одно определение на различный id, сироты (id без ссылок) дропаются как раньше. Детерминизм Yjs только улучшается (для ссылок минтинга больше нет).Зеркала импорта (first-wins для дубль-определений):
packages/editor-ext/src/lib/markdown/utils/footnote.marked.ts→extractFootnoteDefinitions():d__2/d__3;[^a]-маркеры оставить как есть (всеa).packages/mcp/src/lib/collaboration.ts→extractFootnotes(): те же изменения (держать в синхроне с editor-ext — есть явный комментарий «MUST stay in sync»).Фича 2 — Предупреждения
Новая чистая функция
analyzeFootnotes(markdown)(MCP, напр.packages/mcp/src/lib/footnote-analyze.ts)Fence-aware скан (как в
extractFootnotes), возвращает:Правила:
danglingReferences= ссылки, для которых нет определения;duplicateDefinitions= id с >= 2 def-строками;emptyDefinitions= def-строки с пустым(.*)после:;referencesInTables=[^id]найден в строке, чейtrimStart()начинается с|(эвристика, помечается как таковая в тексте предупреждения).Плумбинг — поле
footnoteWarnings: string[]в результат:client.createPage()(client.ts:972): вызватьanalyzeFootnotes(content), приложить к возвращаемому объекту (послеgetPage).client.updatePage()(client.ts:1065): то же поcontent.client.importPageMarkdown()(client.ts:1378): поbody; уже есть полеwarning— добавить рядомfootnoteWarnings.index.ts(create_page— 290,import_page_markdown— 367) прокидывают результат как есть (jsonContent), правок их сигнатур не требуется.createComment/updateComment) — вне объёма (сноски в комментариях нетипичны).Фича 3 — UX редактора
paste-reuse —
footnote-sync.ts→footnotePastePlugin()(:557)remapучитывать, есть ли в слайсеfootnoteDefinitionс этим id; ремапить только такие id (и парные ссылки), одиночные ссылки не трогать.мульти-бэклинки
footnote-numbering.ts/footnote-util.tsкартуid -> reference occurrences(порядок/счётчик), кэшируемую плагином рядом сnumbers. Экспорт gettergetFootnoteRefCount(state, id)(или список индексов).scrollToReference(footnote-reference.ts:318) доscrollToReference(id, index?)→querySelectorAll('sup[data-footnote-ref][data-id="…"]')[index].apps/client/src/features/editor/components/footnote/footnote-definition-view.tsx: при числе ссылок > 1 рендерить ↩ + подметкиa, b, c…, каждая ведёт к своей occurrence; при одной — текущее поведение (один ↩).apps/client/src/features/editor/components/footnote/footnote.module.css— раскладка нескольких бэклинков.Затрагиваемые файлы (чеклист)
packages/editor-ext/src/lib/footnote/footnote-sync.ts— reuse вresolveCollisions, paste-reuse вfootnotePastePluginpackages/editor-ext/src/lib/markdown/utils/footnote.marked.ts—extractFootnoteDefinitions: убрать rewrite маркеров, first-wins для дубль-defpackages/mcp/src/lib/collaboration.ts—extractFootnotes: зеркальные правкиpackages/mcp/src/lib/footnote-analyze.ts— новыйanalyzeFootnotes()packages/mcp/src/client.ts—footnoteWarningsвcreatePage/updatePage/importPageMarkdownpackages/editor-ext/src/lib/footnote/footnote-numbering.ts(+footnote-util.ts) — карта occurrences по idpackages/editor-ext/src/lib/footnote/footnote-reference.ts—scrollToReference(id, index?)apps/client/.../footnote/footnote-definition-view.tsx— мульти-бэклинкиapps/client/.../footnote/footnote.module.css— стили бэклинковТесты
footnote.test.ts— переписать проверки переименования дубль-ссылок на reuse; добавить кейсы: N ссылок + 1 def → один номер/одно определение; ссылка без def → одна пустая сноска (не N штук)footnote.marked.orphan.test.ts— обновить на first-wins (один defd, дубликаты отброшены)analyzeFootnotes— 4 типа диагностик + fence-awarefootnote-views.structure.test.tsx— мульти-бэклинки рендерятся по числу ссылокapps/server/.../footnote-superscript-roundtrip.spec.ts) — не сломанОткрытые пункты для проверки на этапе реализации
create_page(серверный импорт) синтезируются пустышки и сейчас происходит__N: серверный pipeline импорта vs первое открытие в коллаб-редакторе. Рычаг всё равноresolveCollisions, но надо убедиться, что серверный путь его реально исполняет (а не только строит схему черезgenerateJSON).Критерии приёмки (DoD)
[^a]+ одно[^a]: текст→ одна сноска, один номер, N кликабельных ссылок, одно определение; никакихa__2.create_page/update_page/import_page_markdownвозвращаютfootnoteWarningsс указанием id.footnoteWarningsесть запись.[^…]в таблице → попадает вfootnoteWarnings(referencesInTables).Issue подготовлен по результатам разбора бага со сносками (статья про чокопай). Это задача-проектирование; код не менялся.
Ghost referenced this issue2026-06-24 16:06:17 +03:00
Ghost referenced this issue2026-06-24 16:37:25 +03:00
Ghost referenced this issue2026-06-25 12:00:53 +03:00