feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413) #466

Merged
vvzvlad merged 2 commits from feat/413-markdown-default into refactor/412-camelcase-tool-names 2026-07-10 23:45:03 +03:00
Collaborator

Summary

Канонический конвертер (#293/#345/#351) созрел для блочного уровня — делаем markdown форматом по умолчанию для чтения/записи ОДНОГО блока: getNode / patchNode / insertNode. ProseMirror JSON остаётся опцией «для тонких работ». Новых тулов нет, поверхность не растёт. closes #413

Стоит стопкой на #412 (refactor/412-camelcase-tool-names, PR #463) — база PR указывает на ветку #412, поэтому в диффе только изменения #413. Порядок окна: #411#412#413#415.

getNode — markdown по умолчанию + schema-derived фоллбэк

getNode(pageId, nodeId, format='markdown'). markdown (дефолт) оборачивает ноду как {type:doc,content:[node]}convertProseMirrorToMarkdown с дефолтами конвертера, сохраняя comment-якоря включая resolved (это чтение под редактирование, не getPage). json — сырой сабтри (прежнее поведение). Авто-фолбэк не-топ-левел типов (tableRow/Cell/Header по #<index>) в JSON через canBeDocChild = docmostSchema.nodes.doc.contentMatch.matchType(...) — не рукописный список. Поле format на каждом ответе.

patchNode/insertNode — XOR-вход, 1→N сплайс, id-нить, guard

Третий аргумент — XOR {markdown? | node?} (оба optional в схеме, XOR на рантайме; both/neither → чёткая ошибка). markdown-путь: импорт фрагмента → 1→N сплайс (первый блок наследует id цели, остальные — свежие id, disjoint от страницы) → dry replaceNodeById для #159-ambiguity ДО мутации → сплайс через replaceNodeByIdWithMany (patch) / insertNodesRelative (insert). Соседние блоки byte-identical. Guard findUnrepresentableTableAttrs: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы / node-JSON (fail-closed, до мутации).

Сноски

^[...] во фрагменте — первоклассная сноска (канон-импортёр). importMarkdownFragment делит блоки от footnotesList, ремапит id сносок фрагмента в свежие uuid (fn-1 фрагмента не коллизит с fn-1 страницы), mergeFootnoteDefinitions добавляет через ту же машинерию, что insertFootnote. Splice, убравший последний referrer существующей сноски, роняет её orphan-определение (как полный ре-импорт) — сходимость к единому канону держится.

node-ops

replaceNodeByIdWithMany / insertNodesRelative / reassignCollidingBlockIds в prosemirror-markdown/node-ops (канон после #414) + barrel.

How verified

Прогон в wt413 (симлинк-протокол соблюдён, внешний симлинк восстановлен в дефолт):

  • prosemirror-markdown: tsc exit 0; vitest run736 passed (41 файл).
  • mcp: tsc exit 0; node --test test/unit/*.test.mjs test/mock/*.test.mjs702 passed, 0 fail.
  • apps/server: tsc --noEmit -p apps/server/tsconfig.json → чисто (кроме предсуществующего @fastify/compress); jest ai-chat-tools.service shared-tool-specs.contract tool-tiers273 passed.
  • Тесты: сходимость patchNode(markdown) блока docsCanonicallyEqual полному импорту (нет «второго канона»); id-нить 1→N; уникальность top-level block-id; XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved); ^[...]→хвостовой список+перенумерация; orphan-сноска дропается; guard.

Checklist

  • критерии приёмки из #413 выполнены
  • вне заявленного scope ничего не менялось
## Summary Канонический конвертер (#293/#345/#351) созрел для блочного уровня — делаем **markdown форматом по умолчанию** для чтения/записи ОДНОГО блока: `getNode` / `patchNode` / `insertNode`. ProseMirror JSON остаётся опцией «для тонких работ». Новых тулов нет, поверхность не растёт. closes #413 **Стоит стопкой на #412** (`refactor/412-camelcase-tool-names`, PR #463) — база PR указывает на ветку #412, поэтому в диффе только изменения #413. Порядок окна: #411 → #412 → **#413** → #415. ### getNode — markdown по умолчанию + schema-derived фоллбэк `getNode(pageId, nodeId, format='markdown')`. `markdown` (дефолт) оборачивает ноду как `{type:doc,content:[node]}` → `convertProseMirrorToMarkdown` с дефолтами конвертера, сохраняя comment-якоря **включая resolved** (это чтение под редактирование, не `getPage`). `json` — сырой сабтри (прежнее поведение). Авто-фолбэк не-топ-левел типов (tableRow/Cell/Header по `#<index>`) в JSON через `canBeDocChild` = `docmostSchema.nodes.doc.contentMatch.matchType(...)` — не рукописный список. Поле `format` на каждом ответе. ### patchNode/insertNode — XOR-вход, 1→N сплайс, id-нить, guard Третий аргумент — XOR `{markdown? | node?}` (оба optional в схеме, XOR на рантайме; both/neither → чёткая ошибка). markdown-путь: импорт фрагмента → 1→N сплайс (первый блок наследует id цели, остальные — свежие id, disjoint от страницы) → dry `replaceNodeById` для #159-ambiguity ДО мутации → сплайс через `replaceNodeByIdWithMany` (patch) / `insertNodesRelative` (insert). Соседние блоки byte-identical. **Guard** `findUnrepresentableTableAttrs`: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы / `node`-JSON (fail-closed, до мутации). ### Сноски `^[...]` во фрагменте — первоклассная сноска (канон-импортёр). `importMarkdownFragment` делит блоки от `footnotesList`, **ремапит id сносок фрагмента в свежие uuid** (fn-1 фрагмента не коллизит с fn-1 страницы), `mergeFootnoteDefinitions` добавляет через ту же машинерию, что `insertFootnote`. Splice, убравший последний referrer существующей сноски, роняет её orphan-определение (как полный ре-импорт) — сходимость к единому канону держится. ### node-ops `replaceNodeByIdWithMany` / `insertNodesRelative` / `reassignCollidingBlockIds` в `prosemirror-markdown/node-ops` (канон после #414) + barrel. ## How verified Прогон в `wt413` (симлинк-протокол соблюдён, внешний симлинк восстановлен в дефолт): - `prosemirror-markdown`: `tsc` exit 0; `vitest run` → **736 passed** (41 файл). - `mcp`: `tsc` exit 0; `node --test test/unit/*.test.mjs test/mock/*.test.mjs` → **702 passed, 0 fail**. - `apps/server`: `tsc --noEmit -p apps/server/tsconfig.json` → чисто (кроме предсуществующего `@fastify/compress`); `jest ai-chat-tools.service shared-tool-specs.contract tool-tiers` → **273 passed**. - Тесты: сходимость `patchNode(markdown)` блока `docsCanonicallyEqual` полному импорту (нет «второго канона»); id-нить 1→N; уникальность top-level block-id; XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved); `^[...]`→хвостовой список+перенумерация; orphan-сноска дропается; guard. ## Checklist - [x] критерии приёмки из #413 выполнены - [x] вне заявленного scope ничего не менялось
agent_coder added 2 commits 2026-07-10 20:39:12 +03:00
Канонический конвертер (#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>
Правки по внутреннему ревью #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>
Author
Collaborator

Третий линк breaking-окна, стоит стопкой на #412 (PR #463) — база этого PR указывает на refactor/412-camelcase-tool-names, в диффе только #413. Мержить после #463.

Что сделал: markdown — дефолт для блочных getNode/patchNode/insertNode; JSON — опция. getNode с schema-derived фоллбэком не-топ-левел типов; patchNode/insertNode XOR-вход {markdown|node} + 1→N сплайс с id-нитью и guard'ом на непредставимые table-attr; сноски через канон-импортёр с ремапом id и merge той же машиной, что insertFootnote.

Проверка на стенде: pmd tsc+vitest 736, mcp tsc+node --test 702/702, server tsc чисто + jest 273. Ключевой тест — сходимость: patchNode(markdown) блок docsCanonicallyEqual полному импорту (нет «второго канона»).

Внутренний цикл: 2 прохода. Первый нашёл 2 дефекта — (1) WARNING: orphan-сноска не подчищалась, когда фрагмент без сносок убирал последний referrer существующей сноски (ранний return в mergeFootnoteDefinitions пропускал canonicalizeFootnotes) — это ровно «второй канон», который PR декларирует невозможным; (2) NIT: block-id чеканились без сверки с занятыми id страницы. Оба починены (hasFootnoteArtifacts-гейт + reassignCollidingBlockIds). Второй проход (адверсарно на дельту) — APPROVE, регрессий/над-канонизации нет.

Третий линк breaking-окна, стоит стопкой на #412 (PR #463) — база этого PR указывает на `refactor/412-camelcase-tool-names`, в диффе только #413. Мержить после #463. Что сделал: markdown — дефолт для блочных getNode/patchNode/insertNode; JSON — опция. getNode с schema-derived фоллбэком не-топ-левел типов; patchNode/insertNode XOR-вход {markdown|node} + 1→N сплайс с id-нитью и guard'ом на непредставимые table-attr; сноски через канон-импортёр с ремапом id и merge той же машиной, что insertFootnote. Проверка на стенде: pmd tsc+vitest 736, mcp tsc+node --test 702/702, server tsc чисто + jest 273. Ключевой тест — сходимость: patchNode(markdown) блок docsCanonicallyEqual полному импорту (нет «второго канона»). Внутренний цикл: 2 прохода. Первый нашёл 2 дефекта — (1) WARNING: orphan-сноска не подчищалась, когда фрагмент без сносок убирал последний referrer существующей сноски (ранний return в mergeFootnoteDefinitions пропускал canonicalizeFootnotes) — это ровно «второй канон», который PR декларирует невозможным; (2) NIT: block-id чеканились без сверки с занятыми id страницы. Оба починены (hasFootnoteArtifacts-гейт + reassignCollidingBlockIds). Второй проход (адверсарно на дельту) — APPROVE, регрессий/над-канонизации нет.
agent_coder added the review/needs label 2026-07-10 20:39:34 +03:00
Collaborator

Ревью — #466 feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)

Вердикт: PASS

Полный веер из 9 аспектов по ИЗОЛИРОВАННОЙ дельте #413 (база — ветка #412/#463 refactor/412-camelcase-tool-names, НЕ develop; #463 ещё не влит; ревьюил 7cb3199d..e6171a18 = 17 файлов, +1888/−119). Третий линк breaking-окна #416. Гейт зелёный.

Корректность splice/XOR/converter — проверено (клик)

markdown становится ДЕФОЛТНЫМ форматом для блочных getNode/patchNode/insertNode (PM JSON — опция). Новых тулов нет.

  • getNode(format='markdown'): оборачивает ноду {type:doc,content:[node]} → convertProseMirrorToMarkdown, сохраняя comment-якоря ВКЛ resolved (read-for-editing, не getPage — подтверждено: без stripComments). format:'json' → сырой сабтри (прежнее поведение, байт-идентично). Non-top-level (tableRow/Cell/Header по #<index>) АВТО-фолбэк в json через canBeDocChild = docmostSchema.nodes.doc.contentMatch.matchType(...) (schema-derived, верный PM-API; report format:'json'). Поле format на ответе.
  • patchNode/insertNode — XOR {markdown?|node?}: рантайм-XOR (empty-string markdown = отсутствует через trim; both/neither → чёткая ошибка). markdown-путь: importMarkdownFragment → 1→N сплайс in-place (порядок сохранён, соседи byte-identical), первый блок наследует id таргета (skipIndex защищает от re-mint), остальные — свежие id. insertNode position/anchor семантика цела (markdown меняет ЧТО, не ГДЕ).
  • Footnote-convergence (fix-коммит) ТЕРМИНИРУЕТ: fast-path early-return + vetted normalizeAndMergeFootnotes/canonicalizeFootnotes (orphans дропаются), обход через существующий walk — без unbounded-цикла. makeFreshId — bounded do..while(used.has).
  • Round-trip когерентен: getNode(markdown)→edit→patchNode(markdown), docsCanonicallyEqual (блок == тот же markdown импортированный целиком).
Regressions/Security/Testcov — проверено
  • Breaking-flip задокументирован: getNode json→markdown дефолт — в CHANGELOG (окно #411/#412), старое поведение достижимо (format:'json' байт-идентичен). patchNode/insertNode arg-shape XOR — JSON-путь сохранён (patchNodeJson), tool-specs-обёртка транслирует схему→XOR корректно, старые вызыватели с node работают.
  • SECURITY: агентский markdown ВАЛИДИРУЕТСЯ схемой ДО записи — importMarkdownFragment → markdownToProseMirror, затем assertValidNodeShape (#409 findInvalidNode) на каждый блок ДО collab/lock → инъекция типа вне схемы ловится. Resolved-якоря — юзер уже имеет доступ, утечки нет. 1→N сплайс bounded (нет патологического вектора). table-cell с непредставимыми attrs (colspan/rowspan/colwidth/bg) — гард findUnrepresentableTableAttrs.
  • Покрытие полное (538+157+121+119 строк тестов): XOR both/neither→error, 1→1 + 1→N сплайс, id-threading, page-wide uniqueness, orphan-footnote convergence, insert position/anchor, non-top-level fallback, resolved-anchor preservation, table-guard, canBeDocChild.
Объективный гейт (клин-рум /home/claude/gm-ci)
  • pmd vitest 736/736, mcp node --test 702/702 (get-node-format, markdown-patch-insert, markdown-fragment, node-ops-splice), server tsc 0, in-app 39/39.
DROP — кодеру НЕ делать (лог калибровки; честно про адверсариал)
  • [below-threshold] low [test-coverage] адверсариально занейтралил reassignCollidingBlockIds→no-op → uniqueness-тесты (13/14) НЕ покраснели. Т.е. re-mint не форс-тестируется коллизией: уникальность обеспечивается свежими id импортёра (ВОТ load-bearing+tested механизм), а reassignCollidingBlockIds — belt-and-braces на near-impossible fresh-id-коллизию (36^12). Код корректен by inspection (bounded forEach re-mint, skipIndex защищает наследованный id, used дедупит и внутри фрагмента). Рекомендую тест с форсированной коллизией (мок id-генератора) чтобы запинить гард, но не блок — фича-уникальность гарантирована и покрыта.

DO/ESCALATE нет. Splice корректен (in-place/порядок/id), XOR робастен, canBeDocChild верен, footnote-convergence терминирует, markdown валидируется схемой до записи, breaking-flip задокументирован с достижимым json, round-trip когерентен. Гейт зелёный. Approve.

Merge-order: стоит на #463 (#412); мержить после #463 (окно #411→#412→#413→#415). 9 аспектов (stability+coherence, regressions+security+testcov, conv+docs+simp+arch — все LGTM). Splice/XOR/converter/footnote-термин/schema-validation проверены; breaking-flip задокументирован. Dedup-адверсариал не покраснел — honest DROP (uniqueness от fresh-id, re-mint belt-and-braces). Escalate нет.

<!-- state:review reviewed_head=e6171a18 round=1 verdict=approved --> ## Ревью — #466 `feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по ИЗОЛИРОВАННОЙ дельте #413 (база — ветка #412/#463 `refactor/412-camelcase-tool-names`, НЕ develop; #463 ещё не влит; ревьюил `7cb3199d..e6171a18` = 17 файлов, +1888/−119). Третий линк breaking-окна #416. Гейт зелёный. <details> <summary>Корректность splice/XOR/converter — проверено (клик)</summary> markdown становится ДЕФОЛТНЫМ форматом для блочных getNode/patchNode/insertNode (PM JSON — опция). Новых тулов нет. - **getNode(format='markdown')**: оборачивает ноду `{type:doc,content:[node]}` → convertProseMirrorToMarkdown, сохраняя comment-якоря ВКЛ resolved (read-for-editing, не getPage — подтверждено: без stripComments). `format:'json'` → сырой сабтри (прежнее поведение, байт-идентично). Non-top-level (tableRow/Cell/Header по `#<index>`) АВТО-фолбэк в json через `canBeDocChild = docmostSchema.nodes.doc.contentMatch.matchType(...)` (schema-derived, верный PM-API; report `format:'json'`). Поле format на ответе. - **patchNode/insertNode — XOR `{markdown?|node?}`**: рантайм-XOR (empty-string markdown = отсутствует через trim; both/neither → чёткая ошибка). markdown-путь: importMarkdownFragment → **1→N сплайс in-place** (порядок сохранён, соседи byte-identical), первый блок наследует id таргета (skipIndex защищает от re-mint), остальные — свежие id. insertNode position/anchor семантика цела (markdown меняет ЧТО, не ГДЕ). - **Footnote-convergence (fix-коммит) ТЕРМИНИРУЕТ**: fast-path early-return + vetted normalizeAndMergeFootnotes/canonicalizeFootnotes (orphans дропаются), обход через существующий walk — без unbounded-цикла. makeFreshId — bounded `do..while(used.has)`. - **Round-trip когерентен**: getNode(markdown)→edit→patchNode(markdown), `docsCanonicallyEqual` (блок == тот же markdown импортированный целиком). </details> <details> <summary>Regressions/Security/Testcov — проверено</summary> - **Breaking-flip задокументирован**: getNode json→markdown дефолт — в CHANGELOG (окно #411/#412), старое поведение достижимо (`format:'json'` байт-идентичен). patchNode/insertNode arg-shape XOR — JSON-путь сохранён (patchNodeJson), tool-specs-обёртка транслирует схему→XOR корректно, старые вызыватели с `node` работают. - **SECURITY: агентский markdown ВАЛИДИРУЕТСЯ схемой ДО записи** — importMarkdownFragment → markdownToProseMirror, затем `assertValidNodeShape` (#409 findInvalidNode) на каждый блок ДО collab/lock → инъекция типа вне схемы ловится. Resolved-якоря — юзер уже имеет доступ, утечки нет. 1→N сплайс bounded (нет патологического вектора). table-cell с непредставимыми attrs (colspan/rowspan/colwidth/bg) — гард findUnrepresentableTableAttrs. - **Покрытие полное** (538+157+121+119 строк тестов): XOR both/neither→error, 1→1 + 1→N сплайс, id-threading, page-wide uniqueness, orphan-footnote convergence, insert position/anchor, non-top-level fallback, resolved-anchor preservation, table-guard, canBeDocChild. </details> <details> <summary>Объективный гейт (клин-рум /home/claude/gm-ci)</summary> - **pmd vitest 736/736**, **mcp node --test 702/702** (get-node-format, markdown-patch-insert, markdown-fragment, node-ops-splice), **server tsc 0**, in-app **39/39**. </details> <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки; честно про адверсариал)</summary> - `[below-threshold]` `low` **[test-coverage]** адверсариально занейтралил `reassignCollidingBlockIds`→no-op → uniqueness-тесты (13/14) НЕ покраснели. Т.е. re-mint не форс-тестируется коллизией: уникальность обеспечивается свежими id импортёра (ВОТ load-bearing+tested механизм), а reassignCollidingBlockIds — belt-and-braces на near-impossible fresh-id-коллизию (36^12). Код корректен by inspection (bounded forEach re-mint, skipIndex защищает наследованный id, `used` дедупит и внутри фрагмента). Рекомендую тест с форсированной коллизией (мок id-генератора) чтобы запинить гард, но не блок — фича-уникальность гарантирована и покрыта. </details> DO/ESCALATE нет. Splice корректен (in-place/порядок/id), XOR робастен, canBeDocChild верен, footnote-convergence терминирует, markdown валидируется схемой до записи, breaking-flip задокументирован с достижимым json, round-trip когерентен. Гейт зелёный. Approve. <sub>Merge-order: стоит на #463 (#412); мержить после #463 (окно #411→#412→#413→#415). 9 аспектов (stability+coherence, regressions+security+testcov, conv+docs+simp+arch — все LGTM). Splice/XOR/converter/footnote-термин/schema-validation проверены; breaking-flip задокументирован. Dedup-адверсариал не покраснел — honest DROP (uniqueness от fresh-id, re-mint belt-and-braces). Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 20:53:53 +03:00
vvzvlad merged commit c24a781a9d into refactor/412-camelcase-tool-names 2026-07-10 23:45:03 +03:00
Sign in to join this conversation.