feat(mcp): детерминированная нормализация текста сносок + склейка форков #422

Open
agent_coder wants to merge 2 commits from feat/419-footnote-normalize into develop
Collaborator

Summary

Сноски с одинаковым смыслом, но разным начертанием символов (типографские кавычки «…»/"…" vs ASCII, em/en-dash vs -, NBSP vs пробел, разное число пробелов) не распознавались как одинаковые и «форкались» — на странице два определения вместо одного (наблюдалось в проде). Дедуп по содержимому (footnoteContentKey) нормализовал только пробелы; канонизатор дедуплит по attrs.id, а не по тексту.

Добавлен детерминированный серверный пасс normalizeAndMergeFootnotes(doc) (новый чистый модуль packages/mcp/src/lib/footnote-normalize-merge.ts): нормализует текст определений сносок in-place (кавычки→ASCII, тире→-, NBSP/спецпробелы→пробел, схлопывание, trim; марки сохраняются) и сливает определения с совпавшим нормализованным текстом, перевешивая footnoteReference.attrs.id на канонический id (первый в порядке документа). Дубли-сироты добивает существующий canonicalizeFootnotes, который вызывается сразу после. Пасс запаян строго перед canonicalizeFootnotes на всех 5 write-путях MCP (markdown-импорт, updatePageJson, copyPageContent, docmost_transform, insertInlineFootnote). canonicalizeFootnotes не тронут.

closes #419

Ключевые решения (форки для ревью)

  • Ключ слияния attrs-aware. Внутреннее ревью нашло риск потери данных: footnoteContentKey строит сигнатуру марок только по type, поэтому сноски с одинаковым текстом, но link с разным href (или code/highlight с разными attrs) слились бы, потеряв один target — а радиус пасса теперь все full-doc write-пути. Поэтому ключ считается локальным footnoteMergeKey/stableAttrs (type + сортированный JSON.stringify(attrs)), НЕ трогая общий footnoteContentKey (он завязан на живой dedup insertInlineFootnote). Разный href → НЕ сливаются.
  • Нормализация безусловная для всех определений (не только слитых) — как зафиксировано в постановке (#419: «нормализовать текст», «похуй на кавычки», «гарантированно»). Осознанный лоссовый компромисс: у одиночной сноски типографика тоже переписывается в ASCII. Если хочешь только-в-группах-слияния — скажи, переключу.
  • Глиф-карты продублированы из comment-anchor.ts (там private и завязаны на golden-тесты anchor'ов; байт-в-байт идентичны — проверено кодпоинтами внутренним ревью), а не вынесены в общий хелпер, чтобы не менять поведение comment-anchor.

How verified

  • Сборка: pnpm --filter @docmost/mcp build — чисто (build/ gitignored, не коммитил).
  • Новый тест packages/mcp/test/unit/footnote-normalize-merge.test.mjs: node --test …16 passed (кавычки/тире/NBSP/пробелы → одна сноска; bold-vs-plain и разный href → НЕ сливаются; идемпотентность; иммутабельность входа; вложенные в callout; пустые не схлопываются; тело документа не тронуто; многопараграфные; мульти-референс; edge-trim).
  • Регрессия: footnote-canonicalize + footnote-corpus-parity16 passed (twin-тест канонизатора не задет).
  • Полный unit-каталог: 447/449; 2 падения (markdown-converter вложенный bulletList) — пред-существующие на develop, воспроизводятся без моих правок.

Внутренний цикл: 1 проход coder → внутреннее ревью нашло WARNING (потеря target при разном href) → фикс attrs-aware ключа → повторное внутреннее ревью чисто.

Checklist

  • критерии приёмки из #419 выполнены (склейка форков; bold/href не сливаются; ссылки перевешены; идемпотентность; вложенные; пустые; тело не тронуто; 5 call-site)
  • вне заявленного scope ничего не менялось (canonicalizeFootnotes/footnote-authoring не тронуты)
## Summary Сноски с одинаковым смыслом, но разным начертанием символов (типографские кавычки `«…»`/`"…"` vs ASCII, em/en-dash vs `-`, NBSP vs пробел, разное число пробелов) не распознавались как одинаковые и «форкались» — на странице два определения вместо одного (наблюдалось в проде). Дедуп по содержимому (`footnoteContentKey`) нормализовал только пробелы; канонизатор дедуплит по `attrs.id`, а не по тексту. Добавлен детерминированный **серверный** пасс `normalizeAndMergeFootnotes(doc)` (новый чистый модуль `packages/mcp/src/lib/footnote-normalize-merge.ts`): нормализует текст определений сносок in-place (кавычки→ASCII, тире→`-`, NBSP/спецпробелы→пробел, схлопывание, trim; марки сохраняются) и сливает определения с совпавшим нормализованным текстом, перевешивая `footnoteReference.attrs.id` на канонический id (первый в порядке документа). Дубли-сироты добивает существующий `canonicalizeFootnotes`, который вызывается сразу после. Пасс запаян **строго перед** `canonicalizeFootnotes` на всех 5 write-путях MCP (markdown-импорт, `updatePageJson`, `copyPageContent`, `docmost_transform`, `insertInlineFootnote`). `canonicalizeFootnotes` не тронут. closes #419 ## Ключевые решения (форки для ревью) - **Ключ слияния attrs-aware.** Внутреннее ревью нашло риск потери данных: `footnoteContentKey` строит сигнатуру марок только по `type`, поэтому сноски с одинаковым текстом, но `link` с разным `href` (или code/highlight с разными attrs) слились бы, потеряв один target — а радиус пасса теперь все full-doc write-пути. Поэтому ключ считается локальным `footnoteMergeKey`/`stableAttrs` (type + сортированный `JSON.stringify(attrs)`), НЕ трогая общий `footnoteContentKey` (он завязан на живой dedup `insertInlineFootnote`). Разный `href` → НЕ сливаются. - **Нормализация безусловная для всех определений** (не только слитых) — как зафиксировано в постановке (#419: «нормализовать текст», «похуй на кавычки», «гарантированно»). Осознанный лоссовый компромисс: у одиночной сноски типографика тоже переписывается в ASCII. Если хочешь только-в-группах-слияния — скажи, переключу. - **Глиф-карты продублированы** из `comment-anchor.ts` (там `private` и завязаны на golden-тесты anchor'ов; байт-в-байт идентичны — проверено кодпоинтами внутренним ревью), а не вынесены в общий хелпер, чтобы не менять поведение comment-anchor. ## How verified - Сборка: `pnpm --filter @docmost/mcp build` — чисто (build/ gitignored, не коммитил). - Новый тест `packages/mcp/test/unit/footnote-normalize-merge.test.mjs`: `node --test …` → **16 passed** (кавычки/тире/NBSP/пробелы → одна сноска; bold-vs-plain и разный `href` → НЕ сливаются; идемпотентность; иммутабельность входа; вложенные в callout; пустые не схлопываются; тело документа не тронуто; многопараграфные; мульти-референс; edge-trim). - Регрессия: `footnote-canonicalize` + `footnote-corpus-parity` → **16 passed** (twin-тест канонизатора не задет). - Полный unit-каталог: 447/449; 2 падения (`markdown-converter` вложенный bulletList) — пред-существующие на develop, воспроизводятся без моих правок. Внутренний цикл: 1 проход coder → внутреннее ревью нашло WARNING (потеря target при разном href) → фикс attrs-aware ключа → повторное внутреннее ревью чисто. ## Checklist - [x] критерии приёмки из #419 выполнены (склейка форков; bold/href не сливаются; ссылки перевешены; идемпотентность; вложенные; пустые; тело не тронуто; 5 call-site) - [x] вне заявленного scope ничего не менялось (canonicalizeFootnotes/footnote-authoring не тронуты)
agent_coder added 1 commit 2026-07-09 23:24:48 +03:00
Новый чистый модуль footnote-normalize-merge.ts (normalizeAndMergeFootnotes):
нормализует текст определений сносок (типографские кавычки->ASCII, тире->'-',
NBSP/спецпробелы->пробел, схлопывание пробелов, trim) и сливает определения с
совпавшим нормализованным текстом, перевешивая ссылки на канонический id;
дубли-сироты добивает canonicalizeFootnotes. Ключ слияния attrs-aware
(footnoteMergeKey/stableAttrs) — сноски с одинаковым текстом, но разными attrs
марок (напр. link.href) НЕ сливаются (защита от потери target). Пасс вызывается
строго ПЕРЕД canonicalizeFootnotes на 5 write-путях MCP (markdown-импорт,
updatePageJson, copyPageContent, docmost_transform, insertInlineFootnote).
Глиф-карты продублированы из comment-anchor.ts (там private+завязаны на golden).
Идемпотентен, чистый (deep-clone), scope строго внутри footnoteDefinition.

closes #419

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-09 23:24:56 +03:00
Collaborator

Ревью — #422 · feat(mcp): детерминированная нормализация текста сносок + склейка форков (closes #419)

Вердикт: ПРАВКИ (CHANGES) — одна правка. Ядро сделано хорошо: чистый детерминированный normalizeAndMergeFootnotes, attrs-aware merge-key (разный href НЕ сливается — проверено мутацией), 5 write-путей запаяны строго перед canonicalizeFootnotes, glyph-карты байт-в-байт совпадают с comment-anchor.ts (проверено кодпоинтами), тесты 16/16 + регрессия канонизатора 16/16. Но безусловная нормализация переписывает и содержимое inline-code внутри сносок — это порча кода, а не типографики.

Сделать — потом повторное ревью

  • [stability/regressions] Не нормализовать текст под маркой code (порча code-литералов)packages/mcp/src/lib/footnote-normalize-merge.ts:127-133 (normalizeDefinitionText)
    normalizeDefinitionText гонит normalizeAndCollapse по ВСЕМ текст-нодам определения независимо от марок → текст в inline-code тоже переписывается в ASCII (кавычки/тире/спецпробелы). Для code-литерала это не типографика, а изменение смысла. Проверено эмпирически на raw-JSON write-пути (updatePageJson/docmost_transform/copyPageContent): footnoteDefinition с code-нодой "a—b «x»" → после пасса "a-b \"x\"" (порча = true). Особенно бьёт copyPageContent: сноску с inline-code, где автор намеренно поставил Unicode (строка-литерал, em-dash во флаге, i18n), тихо ломает при копировании страницы. Это НЕ покрыто заявленным «осознанным лоссовым компромиссом» (он про типографику одиночной сноски в прозе — про code речи не было) и не покрыто тестами.
    Fix: в normalizeDefinitionText пропускать текст-ноды с маркой code (if ((t.marks||[]).some(m=>m?.type==="code")) continue;) — код остаётся verbatim; merge-key при этом читает сырой code-текст, так что сноски, различающиеся только глифами в коде, корректно НЕ сливаются, а форки в прозе по-прежнему сливаются. Добавить тест: code-нода с типографикой сохраняется байт-в-байт, а проза рядом — нормализуется.

Как проверено (гейт, clean-room /home/claude/gm-ci)

  • pnpm --filter @docmost/mcp build — tsc чисто.
  • node --test footnote-normalize-merge.test.mjs16/16 PASS; мутации подтвердили зубастость: снять stableAttrs → падает тест «разный href не сливается»; снять key==="" continue → падает тест «пустые не схлопываются».
  • регрессия footnote-canonicalize + footnote-corpus-parity16/16 PASS (канонизатор не задет).
  • 5 write-путей: normalizeAndMergeFootnotes строго ПЕРЕД canonicalizeFootnotes на всех (client.ts ×3, collaboration.ts, transforms.ts) — подтверждено; 6-го пути нет.
  • glyph-карты (DOUBLE_QUOTES 9 / SINGLE_QUOTES 4 / DASHES 7 кодпоинтов + isWhitespaceChar 7 спецпробелов) — байт-в-байт == comment-anchor.ts (сверено кодпоинтами).
  • idempotent / вход не мутируется (deep-clone) / тело документа не тронуто / пустые не сливаются / marks сохраняются — прочитано и подтверждено.
DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] medium [simplification] заменить char-loop normalizeAndCollapse на цепочку .replace()footnote-normalize-merge.ts:91-109 — цикл корректен, покрыт тестами и намеренно зеркалит comment-anchor.ts:normalizeForMatch байт-в-байт; regex-переписка разошлась бы с ним и рискует дрейфом набора спецпробелов. Стилевой выбор с плюсом за паритет → не делать.
  • [below-threshold] medium [documentation] коммент «TEXT portion mirrors footnoteContentKey exactly» — footnote-normalize-merge.ts:197-200 — «exactly» относится к тексту/пробелам/trim; расхождение mark-сигнатуры (attrs-aware) подробно объяснено в том же блоке (стр. 181-195). Неточность фразы, не кода.
  • [speculative] [regressions] markdown-путь для code-в-сноске — эмпирически НЕ воспроизвёл code-марку в footnoteDefinition (парсер сносок иначе); реально бьёт raw-JSON путь (см. DO). Оставлено как контекст.

9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: stableAttrs/JSON.stringify без proto-pollution/ReDoS/циклов на schema-constrained входе. Coherence/Architecture: merge→canonicalize handoff здоровый, безусловная нормализация ПРОЗЫ — авторизована #419; порча code — отдельный неучтённый side-effect (см. DO). Escalate нет.

## Ревью — #422 · feat(mcp): детерминированная нормализация текста сносок + склейка форков (closes #419) **Вердикт: ПРАВКИ (CHANGES)** — одна правка. Ядро сделано хорошо: чистый детерминированный `normalizeAndMergeFootnotes`, attrs-aware merge-key (разный `href` НЕ сливается — проверено мутацией), 5 write-путей запаяны строго перед `canonicalizeFootnotes`, glyph-карты байт-в-байт совпадают с `comment-anchor.ts` (проверено кодпоинтами), тесты 16/16 + регрессия канонизатора 16/16. Но безусловная нормализация переписывает и **содержимое inline-`code`** внутри сносок — это порча кода, а не типографики. ### Сделать — потом повторное ревью - **[stability/regressions] Не нормализовать текст под маркой `code` (порча code-литералов)** — `packages/mcp/src/lib/footnote-normalize-merge.ts:127-133` (`normalizeDefinitionText`) `normalizeDefinitionText` гонит `normalizeAndCollapse` по ВСЕМ текст-нодам определения независимо от марок → текст в inline-`code` тоже переписывается в ASCII (кавычки/тире/спецпробелы). Для code-литерала это не типографика, а изменение смысла. **Проверено эмпирически** на raw-JSON write-пути (`updatePageJson`/`docmost_transform`/`copyPageContent`): footnoteDefinition с code-нодой `"a—b «x»"` → после пасса `"a-b \"x\""` (порча = true). Особенно бьёт `copyPageContent`: сноску с inline-code, где автор намеренно поставил Unicode (строка-литерал, em-dash во флаге, i18n), тихо ломает при копировании страницы. Это НЕ покрыто заявленным «осознанным лоссовым компромиссом» (он про типографику одиночной сноски в прозе — про `code` речи не было) и не покрыто тестами. Fix: в `normalizeDefinitionText` пропускать текст-ноды с маркой `code` (`if ((t.marks||[]).some(m=>m?.type==="code")) continue;`) — код остаётся verbatim; merge-key при этом читает сырой code-текст, так что сноски, различающиеся только глифами в коде, корректно НЕ сливаются, а форки в прозе по-прежнему сливаются. Добавить тест: code-нода с типографикой сохраняется байт-в-байт, а проза рядом — нормализуется. ### Как проверено (гейт, clean-room `/home/claude/gm-ci`) - `pnpm --filter @docmost/mcp build` — tsc чисто. - `node --test footnote-normalize-merge.test.mjs` — **16/16 PASS**; мутации подтвердили зубастость: снять `stableAttrs` → падает тест «разный href не сливается»; снять `key===""` continue → падает тест «пустые не схлопываются». - регрессия `footnote-canonicalize` + `footnote-corpus-parity` — **16/16 PASS** (канонизатор не задет). - 5 write-путей: `normalizeAndMergeFootnotes` строго ПЕРЕД `canonicalizeFootnotes` на всех (client.ts ×3, collaboration.ts, transforms.ts) — подтверждено; 6-го пути нет. - glyph-карты (DOUBLE_QUOTES 9 / SINGLE_QUOTES 4 / DASHES 7 кодпоинтов + isWhitespaceChar 7 спецпробелов) — **байт-в-байт == comment-anchor.ts** (сверено кодпоинтами). - idempotent / вход не мутируется (deep-clone) / тело документа не тронуто / пустые не сливаются / marks сохраняются — прочитано и подтверждено. <details><summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` medium **[simplification]** заменить char-loop `normalizeAndCollapse` на цепочку `.replace()` — `footnote-normalize-merge.ts:91-109` — цикл корректен, покрыт тестами и **намеренно зеркалит** `comment-anchor.ts:normalizeForMatch` байт-в-байт; regex-переписка разошлась бы с ним и рискует дрейфом набора спецпробелов. Стилевой выбор с плюсом за паритет → не делать. - `[below-threshold]` medium **[documentation]** коммент «TEXT portion mirrors footnoteContentKey **exactly**» — `footnote-normalize-merge.ts:197-200` — «exactly» относится к тексту/пробелам/trim; расхождение mark-сигнатуры (attrs-aware) подробно объяснено в том же блоке (стр. 181-195). Неточность фразы, не кода. - `[speculative]` **[regressions]** markdown-путь для code-в-сноске — эмпирически НЕ воспроизвёл code-марку в footnoteDefinition (парсер сносок иначе); реально бьёт raw-JSON путь (см. DO). Оставлено как контекст. </details> <sub>9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: `stableAttrs`/JSON.stringify без proto-pollution/ReDoS/циклов на schema-constrained входе. Coherence/Architecture: merge→canonicalize handoff здоровый, безусловная нормализация ПРОЗЫ — авторизована #419; порча `code` — отдельный неучтённый side-effect (см. DO). Escalate нет.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-09 23:38:05 +03:00
agent_coder added 1 commit 2026-07-09 23:46:05 +03:00
Ревью #422: безусловная нормализация переписывала в ASCII и текст inline-code
внутри сносок (кавычки/тире/спецпробелы) — для code-литерала это не типографика,
а изменение смысла (эмпирически на raw-JSON путях: code-нода "a—b «x»" -> "a-b").
normalizeDefinitionText теперь пропускает текст-ноды с маркой code (verbatim), и
краевой trim определения тоже не трогает крайние code-ноды. footnoteMergeKey
читает сырой текст code-нод -> сноски, различающиеся только глифами в коде, НЕ
сливаются, а форки в прозе по-прежнему сливаются. Тесты: code verbatim +
непослияние по code-глифам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Поправил единственную запрошенную правку.

[stability/regressions] Не нормализовать текст под маркой code — исправлено (db2ed95e).

normalizeDefinitionText теперь пропускает текст-ноды с маркой code (if ((t.marks||[]).some(m=>m?.type==="code")) continue;) — code-литерал остаётся verbatim. Дополнительно загардил краевой trim определения: он независимо мутировал первый/последний текст-нод, поэтому code-нода на краю всё равно теряла бы ведущие/хвостовые пробелы — теперь trim пропускает крайние code-ноды. footnoteMergeKey не менялся: читает сырой текст code-нод, так что сноски, различающиеся только глифами в коде, корректно НЕ сливаются, а форки в прозе по-прежнему сливаются (выпадает естественно).

Тесты (2 новых):

  • code-нода с типографикой ("a—b «x»") + проза рядом → после пасса код байт-в-байт цел, проза нормализована в ASCII;
  • две сноски, различающиеся только глифами внутри code («x» vs "x") → НЕ сливаются (оба def живут до и после канонизации).

Проверка (clean-room): pnpm --filter @docmost/mcp build чисто; node --test footnote-normalize-merge + footnote-canonicalize + footnote-corpus-parity34/34 PASS.

Внутренний цикл на правку: coder → внутреннее ревreview (verbatim end-to-end + идемпотентность смешанных code/prose краёв + тип марки "code") → APPROVE, багов нет.

Готово к повторному ревью.

Поправил единственную запрошенную правку. **[stability/regressions] Не нормализовать текст под маркой `code`** — исправлено (`db2ed95e`). `normalizeDefinitionText` теперь пропускает текст-ноды с маркой `code` (`if ((t.marks||[]).some(m=>m?.type==="code")) continue;`) — code-литерал остаётся verbatim. Дополнительно загардил **краевой trim определения**: он независимо мутировал первый/последний текст-нод, поэтому code-нода на краю всё равно теряла бы ведущие/хвостовые пробелы — теперь trim пропускает крайние code-ноды. `footnoteMergeKey` не менялся: читает сырой текст code-нод, так что сноски, различающиеся только глифами в коде, корректно НЕ сливаются, а форки в прозе по-прежнему сливаются (выпадает естественно). Тесты (2 новых): - code-нода с типографикой (`"a—b «x»"`) + проза рядом → после пасса код байт-в-байт цел, проза нормализована в ASCII; - две сноски, различающиеся только глифами внутри `code` (`«x»` vs `"x"`) → НЕ сливаются (оба def живут до и после канонизации). Проверка (clean-room): `pnpm --filter @docmost/mcp build` чисто; `node --test footnote-normalize-merge + footnote-canonicalize + footnote-corpus-parity` → **34/34 PASS**. Внутренний цикл на правку: coder → внутреннее ревreview (verbatim end-to-end + идемпотентность смешанных code/prose краёв + тип марки "code") → APPROVE, багов нет. Готово к повторному ревью.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-09 23:46:25 +03:00
Collaborator

Ревью — #422 (fix-round) · нормализация сносок: код-марки

Вердикт: OK (PASS). Правка из прошлого раунда внесена точно и полно; проверено адверсариально моим же исходным пробом — новых багов фикс не внёс.

Что проверено (head db2ed95e, дельта 845ef9af..db2ed95e)

  • Дельта хирургическая: только footnote-normalize-merge.ts (+25/-5) + его тест (+46). normalizeDefinitionText теперь пропускает текст-ноды с маркой code (и edge-trim тоже гардит hasCodeMark) → inline-код остаётся verbatim.
  • Тот же адверсариальный проб, что вскрыл баг: raw-JSON footnoteDefinition с code-нодой "a—b «x»" → после пасса "a—b «x»" (PRESERVED). Порча устранена.
  • Регрессия-гвард ядра #419: prose-глиф-форки («hi» vs "hi", без code) по-прежнему сливаются (обе ссылки → A). Слияние форков не сломано.
  • Новые тесты (+2): «code verbatim, проза рядом нормализуется» (code-нода a—b «x» не тронута, проза → prose "y" - z); «две сноски, различающиеся ТОЛЬКО глифами в коде, НЕ сливаются» (обе выживают + canonicalize их сохраняет). Merge-key корректно читает сырой code-текст.
  • Гейт: mcp build чисто; footnote-normalize-merge 18/18; footnote-canonicalize + corpus-parity регрессия 16/16.

Единственный DO прошлого раунда закрыт, побочек нет. Апрув. Лейбл → review/approved.

## Ревью — #422 (fix-round) · нормализация сносок: код-марки **Вердикт: OK (PASS).** Правка из прошлого раунда внесена точно и полно; проверено адверсариально моим же исходным пробом — новых багов фикс не внёс. ### Что проверено (head `db2ed95e`, дельта `845ef9af..db2ed95e`) - Дельта хирургическая: **только** `footnote-normalize-merge.ts` (+25/-5) + его тест (+46). `normalizeDefinitionText` теперь пропускает текст-ноды с маркой `code` (и edge-trim тоже гардит `hasCodeMark`) → inline-код остаётся verbatim. - **Тот же адверсариальный проб, что вскрыл баг**: raw-JSON footnoteDefinition с code-нодой `"a—b «x»"` → после пасса **`"a—b «x»"` (PRESERVED)**. Порча устранена. - **Регрессия-гвард ядра #419**: prose-глиф-форки (`«hi»` vs `"hi"`, без code) по-прежнему **сливаются** (обе ссылки → A). Слияние форков не сломано. - Новые тесты (+2): «code verbatim, проза рядом нормализуется» (code-нода `a—b «x»` не тронута, проза → `prose "y" - z`); «две сноски, различающиеся ТОЛЬКО глифами в коде, НЕ сливаются» (обе выживают + canonicalize их сохраняет). Merge-key корректно читает сырой code-текст. - Гейт: mcp build чисто; `footnote-normalize-merge` **18/18**; `footnote-canonicalize` + `corpus-parity` регрессия **16/16**. Единственный DO прошлого раунда закрыт, побочек нет. Апрув. Лейбл → `review/approved`.
agent_reviewer added review/approved and removed review/needs labels 2026-07-09 23:51:36 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/419-footnote-normalize:feat/419-footnote-normalize
git checkout feat/419-footnote-normalize
Sign in to join this conversation.