refactor(client): вставка markdown через канонический пакет + удаление md-слоя editor-ext (#347) #498

Merged
vvzvlad merged 3 commits from refactor/347-client-md-paste into develop 2026-07-11 04:33:39 +03:00
Collaborator

Summary

Третий (технически нетривиальный) шаг #345: клиентская вставка markdown переезжает с md-слоя editor-ext (marked, не знал канона — ^[…], <!--img {…}-->, <!--subpages--> при вставке не распознавались) на канонический @docmost/prosemirror-markdown. По завершении md-слой editor-ext удалён целиком. closes #347

Browser-entry пакета (инъекция DOM — jsdom только в Node)

dom-parser.ts — 2 слота инъекции (HtmlDocumentParser, GenerateJsonFn), без импорта DOM. dom-parser.node.ts регистрирует jsdom + @tiptap/html/server; dom-parser.browser.ts — нативный DOMParser + @tiptap/html (browser), экспонирован через exports-условие "browser" + сабпас "./browser". Клиент всегда импортирует явный сабпас /browser (не полагается на порядок условий). Node-потребители (mcp/server) идут по "."defaultindex.js→jsdom — не затронуты.

markdown-clipboard.ts (шов сохранён)

Изменился только ИСТОЧНИК: markdownToProseMirror (browser) → PM-JSON → HTML через живую схему DOMSerializerнеизменённый downstream (normalizeTableColumnWidths→parseSlice→canonicalizePastedFootnotes→dispatch). Эвристики/fragment-insertion не тронуты. Конвертер async → handlePaste захватывает диапазон, забирает событие, диспатчит на резолве; обе ветки (success + fail-open) защищены guard'ом doc!==startDoc.

Удаление md-слоя

packages/editor-ext/src/lib/markdown/ удалён целиком (+ marked из package.json, turndown ушёл из бандла). Мигрированы ВСЕ потребители: ai-chat/utils/markdown.ts (→ новый markdownToProseMirrorSync), use-generate-page-title.ts/page-header-menu.tsx (→ convertProseMirrorToMarkdown), серверный spec. editor-ext = только схема/расширения.

How verified

  • prosemirror-markdown tsc чисто; vitest 744; клиентская сборка успешна, grep бандла dist/assets/*.js на new JSDOM|parse5|GlobalWindow|happy-dom|TurndownService0 (утечки нет).
  • Юнит: dual-path parity (jsdom==DOMParser для 6 канон-форм), канон == серверный импорт, негативы ($5/==/[^1] остаются литералом), async-paste (claim→convert→dispatch + fail-open). Клиентский suite + paste/canonicalize 34; editor-ext 196; node-потребители (mcp/server/git-sync) зелёные.
  • AI-чат: рендер markdown теперь через схему редактора (<li><p>), добавлен scoped .markdown li p{margin:0} — тесты обновлены. Визуальная QA чата — на стенде.

⚠️ Требует ручной проверки (вне автостенда)

  • Ручной paste-QA полного клиентского round-trip: вставка из VSCode/Obsidian/GitHub; вставка ВНУТРЬ списка/ячейки таблицы/callout; экзотические узлы (footnote/callout/math/image-comment) через полную схему редактора; plain-текст с md-пунктуацией — без фантомной конверсии; async-вставка попадает под курсор, undo одним шагом. Это шов, где живут регрессии (§#347).
  • Docker/CI-сборка клиента (#333-класс: «зелёное в workspace ≠ зелёное в CI/Docker») — я проверил workspace-сборку и отсутствие статического импорта jsdom, но прод-образ не собирал.

Checklist

  • Вставка канон-md даёт те же ноды, что серверный импорт (общая фикстура, оба через markdownToProseMirror)
  • Бандл клиента без jsdom/turndown (grep-доказательство)
  • md-слой editor-ext удалён; editor-ext — только схема/расширения
  • Ручной paste-QA + Docker-сборка — за ревьюером/владельцем (вне автостенда)
## Summary Третий (технически нетривиальный) шаг #345: **клиентская вставка markdown** переезжает с md-слоя editor-ext (marked, не знал канона — `^[…]`, `<!--img {…}-->`, `<!--subpages-->` при вставке не распознавались) на канонический `@docmost/prosemirror-markdown`. По завершении md-слой editor-ext **удалён целиком**. closes #347 ### Browser-entry пакета (инъекция DOM — jsdom только в Node) `dom-parser.ts` — 2 слота инъекции (`HtmlDocumentParser`, `GenerateJsonFn`), без импорта DOM. `dom-parser.node.ts` регистрирует jsdom + `@tiptap/html/server`; `dom-parser.browser.ts` — нативный `DOMParser` + `@tiptap/html` (browser), экспонирован через `exports`-условие `"browser"` + сабпас `"./browser"`. Клиент **всегда** импортирует явный сабпас `/browser` (не полагается на порядок условий). Node-потребители (mcp/server) идут по `"."`→`default`→`index.js`→jsdom — не затронуты. ### markdown-clipboard.ts (шов сохранён) Изменился только ИСТОЧНИК: `markdownToProseMirror` (browser) → PM-JSON → HTML через живую схему `DOMSerializer` → **неизменённый** downstream (`normalizeTableColumnWidths→parseSlice→canonicalizePastedFootnotes→dispatch`). Эвристики/fragment-insertion не тронуты. Конвертер async → `handlePaste` захватывает диапазон, забирает событие, диспатчит на резолве; **обе** ветки (success + fail-open) защищены guard'ом `doc!==startDoc`. ### Удаление md-слоя `packages/editor-ext/src/lib/markdown/` удалён целиком (+ `marked` из package.json, turndown ушёл из бандла). Мигрированы ВСЕ потребители: `ai-chat/utils/markdown.ts` (→ новый `markdownToProseMirrorSync`), `use-generate-page-title.ts`/`page-header-menu.tsx` (→ `convertProseMirrorToMarkdown`), серверный spec. editor-ext = только схема/расширения. ## How verified - `prosemirror-markdown` tsc чисто; vitest **744**; **клиентская сборка успешна**, grep бандла `dist/assets/*.js` на `new JSDOM|parse5|GlobalWindow|happy-dom|TurndownService` → **0** (утечки нет). - Юнит: dual-path parity (jsdom==DOMParser для 6 канон-форм), канон == серверный импорт, **негативы** (`$5`/`==`/`[^1]` остаются литералом), async-paste (claim→convert→dispatch + fail-open). Клиентский suite + paste/canonicalize **34**; editor-ext **196**; node-потребители (mcp/server/git-sync) зелёные. - **AI-чат**: рендер markdown теперь через схему редактора (`<li><p>`), добавлен scoped `.markdown li p{margin:0}` — тесты обновлены. Визуальная QA чата — на стенде. ### ⚠️ Требует ручной проверки (вне автостенда) - **Ручной paste-QA** полного клиентского round-trip: вставка из VSCode/Obsidian/GitHub; вставка ВНУТРЬ списка/ячейки таблицы/callout; экзотические узлы (footnote/callout/math/image-comment) через полную схему редактора; plain-текст с md-пунктуацией — без фантомной конверсии; async-вставка попадает под курсор, undo одним шагом. Это шов, где живут регрессии (§#347). - **Docker/CI-сборка клиента** (#333-класс: «зелёное в workspace ≠ зелёное в CI/Docker») — я проверил workspace-сборку и отсутствие статического импорта jsdom, но прод-образ не собирал. ## Checklist - [x] Вставка канон-md даёт те же ноды, что серверный импорт (общая фикстура, оба через markdownToProseMirror) - [x] Бандл клиента без jsdom/turndown (grep-доказательство) - [x] md-слой editor-ext удалён; editor-ext — только схема/расширения - [ ] Ручной paste-QA + Docker-сборка — за ревьюером/владельцем (вне автостенда)
agent_coder added 1 commit 2026-07-11 02:12:01 +03:00
Третий шаг #345: клиентская вставка markdown переезжает с marked-слоя editor-ext
(не знал канона — ^[…], <!--img {…}-->, <!--subpages--> при вставке не
распознавались) на канонический пакет. По завершении слой удалён целиком. closes #347

- Browser-entry пакета (инъекция DOM-парсера): jsdom только в Node-пути.
  dom-parser.ts — 2 слота инъекции (HtmlDocumentParser для HTML→Document,
  GenerateJsonFn для @tiptap/html), без импорта DOM. dom-parser.node.ts
  регистрирует jsdom + @tiptap/html/server; dom-parser.browser.ts — нативный
  DOMParser + @tiptap/html (browser), экспонирован через exports-условие
  "browser" + сабпас "./browser". markdown-to-prosemirror.ts: убраны статический
  импорт jsdom и module-level global.window-шим. Клиент ВСЕГДА импортирует явный
  сабпас /browser — не полагается на порядок условий. Node-потребители (mcp/
  server) идут по "." → default → index.js → jsdom, не затронуты.
- markdown-clipboard.ts: конвертация через browser-entry (markdownToProseMirror
  → PM-JSON → HTML через живую схему редактора DOMSerializer → НЕИЗМЕНЁННЫЙ
  downstream-шов normalizeTableColumnWidths→parseSlice→canonicalizePastedFootnotes
  →dispatch). Эвристики/fragment-insertion не тронуты. Конвертер async → handle
  Paste захватывает диапазон, забирает событие, диспатчит на резолве; и success,
  и fail-open ветки защищены guard'ом doc!==startDoc (не диспатчить по устаревшему
  диапазону). clipboardTextSerializer (copy PM→md) — через convertProseMirror
  ToMarkdown.
- Удалён packages/editor-ext/src/lib/markdown/ целиком (+ marked из package.json).
  Мигрированы ВСЕ потребители markdownToHtml/htmlToMarkdown: ai-chat/utils/
  markdown.ts (→ новый markdownToProseMirrorSync + DOMSerializer), use-generate-
  page-title.ts / page-header-menu.tsx (→ convertProseMirrorToMarkdown(getJSON)),
  серверный spec. Grep: осиротевших импортов нет, editor-ext = только схема/
  расширения. Turndown ушёл из бандла (был в старом htmlToMarkdown).
- AI-чат теперь рендерит markdown через схему редактора (li в <p>); добавлен
  .markdown li p{margin:0} (CSS-модуль, скоуп только чата) — визуально плотно.

Проверка: pmd tsc + vitest 744; client build УСПЕШЕН, grep бандла на
JSDOM/parse5/happy-dom/turndown — 0 (утечки нет); клиентский suite + paste-тесты
зелёные (34); editor-ext 196; node-потребители (mcp/server/git-sync) зелёные.
Юнит-тесты: dual-path parity (jsdom==DOMParser), канон-формы == серверный импорт,
негативы ($5/==/[^1] не корёжатся), async-paste (claim→convert→dispatch, fail-open).

Ручная paste-QA полного клиентского round-trip (footnote/callout/math/image-comment;
вставка из VSCode/Obsidian/GitHub в список/таблицу/callout) и Docker-сборка клиента
(#333-класс) — за пределами автостенда, оставлено на ручную проверку.

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

Готово к ревью. Клиентская вставка markdown переехала на канонический пакет через новый browser-entry (нативный DOMParser, jsdom только в Node), md-слой editor-ext удалён целиком, все потребители мигрированы. Критерии #347 выполнены на автостенде.

Внутренний цикл: 2 прохода. Первый (адверсарно, фокус на утечку jsdom в клиентский бандл #333-класса и сохранение async paste-шва) — утечки нет (grep бандла чист: 0 JSDOM/parse5/happy-dom/turndown), клиент импортирует явный сабпас /browser, downstream-шов вставки байт-в-байт сохранён, node-потребители зелёные; блокеров нет. Кодер по ходу поймал 2 реальных бага browser-пути (defaultView===null у нативного Document; @tiptap/html generateJSON на Node тянул browser-entry без window — починил инъекцией @tiptap/html/server). Второй проход (полиш): fail-open ветка вставки теперь тоже под guard'ом doc!==startDoc (не бросит RangeError по устаревшему диапазону → без unhandled rejection на горячем пути).

⚠️ Требует РУЧНОЙ проверки (вне автостенда) — важно

Автотесты покрывают конвертер и негативы, но НЕ полный клиентский round-trip вставки через живую схему редактора. Прошу на стенде:

  • Вставка из VSCode / Obsidian / GitHub; вставка ВНУТРЬ списка / ячейки таблицы / callout; экзотические узлы (footnote / callout / math / image-comment).
  • Plain-текст с md-пунктуацией ($5, ==, [^1]) — не должен внезапно конвертироваться.
  • Async-вставка попадает под курсор, undo одним шагом.
  • Визуальная QA рендера AI-чата после смены на схему редактора (<li><p> + scoped CSS): списки/таблицы/код в ответах.
  • Docker/CI-сборка клиента (#333-класс) — я проверил только workspace-сборку + отсутствие статического импорта jsdom.
Готово к ревью. Клиентская вставка markdown переехала на канонический пакет через новый browser-entry (нативный DOMParser, jsdom только в Node), md-слой editor-ext удалён целиком, все потребители мигрированы. Критерии #347 выполнены на автостенде. Внутренний цикл: 2 прохода. Первый (адверсарно, фокус на утечку jsdom в клиентский бандл #333-класса и сохранение async paste-шва) — утечки нет (grep бандла чист: 0 JSDOM/parse5/happy-dom/turndown), клиент импортирует явный сабпас /browser, downstream-шов вставки байт-в-байт сохранён, node-потребители зелёные; блокеров нет. Кодер по ходу поймал 2 реальных бага browser-пути (defaultView===null у нативного Document; @tiptap/html generateJSON на Node тянул browser-entry без window — починил инъекцией @tiptap/html/server). Второй проход (полиш): fail-open ветка вставки теперь тоже под guard'ом doc!==startDoc (не бросит RangeError по устаревшему диапазону → без unhandled rejection на горячем пути). <details> <summary>⚠️ Требует РУЧНОЙ проверки (вне автостенда) — важно</summary> Автотесты покрывают конвертер и негативы, но НЕ полный клиентский round-trip вставки через живую схему редактора. Прошу на стенде: - Вставка из VSCode / Obsidian / GitHub; вставка ВНУТРЬ списка / ячейки таблицы / callout; экзотические узлы (footnote / callout / math / image-comment). - Plain-текст с md-пунктуацией (`$5`, `==`, `[^1]`) — не должен внезапно конвертироваться. - Async-вставка попадает под курсор, undo одним шагом. - Визуальная QA рендера AI-чата после смены на схему редактора (`<li><p>` + scoped CSS): списки/таблицы/код в ответах. - Docker/CI-сборка клиента (#333-класс) — я проверил только workspace-сборку + отсутствие статического импорта jsdom. </details>
agent_coder added the review/needs label 2026-07-11 02:12:34 +03:00
Collaborator

Ревью #498 — вставка markdown через канонический пакет + удаление md-слоя. Вердикт: changes-requested

Крепкий рефактор, направление верное, и главное — изоляция bundle подтверждена реальной сборкой, а не только дизайном. Корректных багов нет: async-paste безопасен (конвертер sync-core → резолв на микротаске, интерливинг невозможен), XSS двойно загейчен схемой+DOMPurify, downstream-шов не тронут, удаление слоя ничего не оставило висеть. Блокирует пять некрупных, но реальных пунктов: одна визуальная регрессия (которую сам PR чинил, но пропустил вторую поверхность), «глухой» catch без диагностики, непокрытый async-guard и рассинхрон доков/комментов.

Открыто: F1 (регрессия отступов списков в панели Reasoning), F2 (глухой catch вставки без логов), F3 (нет теста на doc-changed guard), F4 (устаревшие/неверные комменты), F5 (внешние доки не отражают нового клиента-потребителя).

Что проверил (объективка + верификация)
  • Гейт (чистая комната, реальная сборка): prosemirror-markdown tsc+build ок, vitest 744/744; editor-ext vitest 196/196 (удаление слоя чистое — ничто не ссылается); КЛИЕНТСКАЯ СБОРКА успешна; grep бандла dist/assets/*.js на JSDOM|parse5|GlobalWindow|happy-dom|TurndownService → 0 (утечки нет — эмпирически, не только по дизайну); клиентский paste/canonicalize 34/34; mcp tsc ок.
  • Изоляция — статическая, а не на tree-shaking: единственный import jsdom в dom-parser.node.ts, грузится ТОЛЬКО из default-энтри index.ts; клиент во ВСЕХ местах импортит явный сабпас @docmost/prosemirror-markdown/browserbrowser.ts → нативный DOMParser, jsdom недостижим. sideEffects не выставлен (=true) → регистрирующий side-effect импорт не выпиливается.
  • Async-paste — НЕ баг: markdownToProseMirror объявлен async, но тело = return markdownToProseMirrorSync(...) без await → промис резолвится на микротаске, до любого макротаска (следующий paste/collab-апдейт/клавиша). Интерливинг двух вставок и ветка doc!==startDoc в проде недостижимы. (Security+coherence подтвердили.)
  • Миграция потребителей эквивалентна: page-title/header-menu htmlToMarkdown(getHTML)convertProseMirrorToMarkdown(getJSON) — оба sync, расхождение только там, где turndown≠канон (намеренная конвергенция к export-форме). Серверный spec — тест-онли правка. Потерянное покрытие удалённых *.marked тестов полностью перекрыто в packages/prosemirror-markdown/test/ (footnote/math/callout/spoiler/html-embed). Негативы ($5/==/[^1]) покрыты на новом пути.

🔧 Сделать, потом ре-ревью

  1. F1 [regressions] Регрессия отступов списков в панели Reasoning — CSS-фикс поставлен только для .markdown, пропущен .reasoningText. apps/client/src/features/ai-chat/components/ai-chat.module.css:63 (.markdown li p{margin:0}) vs :180 (.reasoningText p{margin:0 0 4px}). Миграция заставила renderChatMarkdown всегда эмитить <li><p>…</p></li>. Тело ответа (message-item.tsx, класс .markdown) — покрыто новым правилом; панель размышлений (reasoning-block.tsx:56,90 — тот же renderChatMarkdown, класс .reasoningText) правила .reasoningText li p НЕ имеет → каждый <p> внутри <li> ловит .reasoningText p's margin:0 0 4px = лишние 4px на пункт. Раньше marked давал <li>text</li> без <p>, было плотно. Это ровно та регрессия, которую CSS-фикс PR и должен был снять, но пропустил на второй поверхности.
    Фикс: добавить .reasoningText li p{margin:0} зеркально .markdown li p.

  2. F2 [stability] Fail-open .catch вставки глотает ошибку без диагностики. markdown-clipboard.ts:180.catch(() => {…}) без аргумента и без лога, ловит ошибки И конвертера, И тела success-.then (в т.ч. PMNode.fromJSON(schema, doc), который бросает при дрейфе схемы между каноническим пакетом и живой схемой редактора). При сбое вставка тихо деградирует в сырой текст (fail-open — данные не теряются, хорошо), но БЕЗ console/телеметрии → невидимая регрессия «моя таблица вставилась текстом», невоспроизводимая для разработчиков.
    Фикс: .catch((err) => { console.error("markdown paste conversion failed, inserting raw text", err); … }) (или через app error-reporter).

  3. F3 [test-coverage] Async doc-changed guard (обе ветки) + две вставки в полёте — не покрыты. Суть нового async-шва — remap-гард view.state.doc !== startDoc на success (:163) и fail-open (:192). markdown-clipboard.paste.test.ts (3 теста) НИ в одном не меняет doc между синхронным claim и микротаском .then. Fail-open ветка со stale-from/to (её же коммент предупреждает про RangeError/unhandled-rejection) — ноль покрытия. Достижимо в тесте: вызвать handlePaste, до flush() диспатчнуть doc-меняющую транзакцию.
    Фикс: два теста — (a) success: doc сменился mid-flight → контент лёг в живую селекцию без клоббера/исключения; (b) fail-open: форсить сбой конверсии с изменённым doc → сырой текст переставлен без RangeError. Плюс тест «две вставки в полёте — оба пейлоада выживают».

  4. F4 [documentation] Устаревшие/неверные комментарии.

    • markdown-clipboard.ts:106-111,159: «marked pipeline is declared async» — фактически неверно, конвертер SYNC (async-обёртка сохранена лишь ради Node-контракта). И «map the range through doc steps» переоценивает — код не StepMap-мапит, а падает на живую селекцию. Переформулировать («returns a Promise, kept async for the Node contract; pipeline is sync» / «fall back to the live selection»).
    • editor-ext/src/lib/footnote/footnote-sync.ts:106 (и мягче footnote-util.ts:66, +тест): комментарий ссылается на удалённый этим PR footnote.marked.ts. Перенаправить на @docmost/prosemirror-markdown.
  5. F5 [documentation] Внешние доки не отражают нового клиента-потребителя канона. AGENTS.md:336-337,463 и packages/prosemirror-markdown/README.md:3-9 утверждают «no browser runtime … consumed by mcp, git-sync, apps/server» + «do NOT reintroduce a per-package copy» — теперь ложно: apps/client потребляет пакет через /browser. Добавить apps/client (через browser-энтри, нативный DOMParser, без jsdom в бандле) и снять «no browser runtime». Плюс запись в CHANGELOG.md [Unreleased] (клиентская вставка markdown теперь через канонический конвертер; удалён marked/turndown md-слой) — репо ведёт changelog для paste-изменений (ср. #192).

DROP — кодеру НЕ делать · калибровка (оператору)
  • [intended] low/med [regressions] Сырой inline-HTML из LLM (<kbd>,<sub>,произвольные <span>) теперь дропается schema-round-trip'ом (текст сохраняется). Намеренная конвергенция к каноничной схеме; чат — untrusted+санитайзится. Принять.
  • [speculative] low/high [stability] Будущий реальный await в конвертере превратит мёртвый doc-changed guard в живой клоббер (fallback берёт сырую текущую селекцию). Не баг сегодня (sync-core); F3 частично пиннит инвариант.
  • [below-threshold] low/high [test-coverage] Parity-тест dom-parser-both-paths свопает только DOM-parser, не generateJson — обе ноги гоняют node(happy-dom) generateJSON; кросс-движковое расхождение в HTML→PM-JSON пройдёт тихо. Но покрыт вероятный источник расхождения (parser-проходы); дивергенция generateJSON удалённа.
  • [below-threshold] low/med [test-coverage] Нет закоммиченного regression-теста «нет jsdom в клиентском бандле» — инвариант держится grep-гейтом + статической сепарацией графа; долговечность nice-to-have.
  • [speculative] low/high [architecture] Нет поля sideEffects — безопасно сегодня (дефолт true), но будущий "sideEffects": false выпилит регистрирующий импорт browser.ts → рантайм-throw. Стоит пин/коммент.
  • [below-threshold] suggestion/low [simplification] Дублирование range-guard в обеих ветках .then/.catch → вынести хелпер. И: инъекцию можно заменить browser-field remap'ом — текущая структура защитима, автор вправе оставить.

9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence) + гейт с реальной клиентской сборкой и grep бандла (0 утечек). security/stability(crux)/architecture/coherence — корректность и изоляция чисты. Все F дёшевы; на ре-ревью прогоню тем же прогоном + мутацию async-guard.

## Ревью #498 — вставка markdown через канонический пакет + удаление md-слоя. Вердикт: **changes-requested** Крепкий рефактор, направление верное, и главное — **изоляция bundle подтверждена реальной сборкой**, а не только дизайном. Корректных багов нет: async-paste безопасен (конвертер sync-core → резолв на микротаске, интерливинг невозможен), XSS двойно загейчен схемой+DOMPurify, downstream-шов не тронут, удаление слоя ничего не оставило висеть. Блокирует пять некрупных, но реальных пунктов: одна визуальная регрессия (которую сам PR чинил, но пропустил вторую поверхность), «глухой» catch без диагностики, непокрытый async-guard и рассинхрон доков/комментов. Открыто: **F1** (регрессия отступов списков в панели Reasoning), **F2** (глухой catch вставки без логов), **F3** (нет теста на doc-changed guard), **F4** (устаревшие/неверные комменты), **F5** (внешние доки не отражают нового клиента-потребителя). <details><summary><b>✅ Что проверил (объективка + верификация)</b></summary> - **Гейт (чистая комната, реальная сборка):** `prosemirror-markdown` tsc+build ок, vitest **744/744**; `editor-ext` vitest **196/196** (удаление слоя чистое — ничто не ссылается); **КЛИЕНТСКАЯ СБОРКА успешна**; **grep бандла `dist/assets/*.js` на `JSDOM|parse5|GlobalWindow|happy-dom|TurndownService` → 0** (утечки нет — эмпирически, не только по дизайну); клиентский paste/canonicalize **34/34**; mcp tsc ок. - **Изоляция — статическая, а не на tree-shaking:** единственный `import jsdom` в `dom-parser.node.ts`, грузится ТОЛЬКО из default-энтри `index.ts`; клиент во ВСЕХ местах импортит явный сабпас `@docmost/prosemirror-markdown/browser` → `browser.ts` → нативный `DOMParser`, jsdom недостижим. `sideEffects` не выставлен (=true) → регистрирующий side-effect импорт не выпиливается. - **Async-paste — НЕ баг:** `markdownToProseMirror` объявлен async, но тело = `return markdownToProseMirrorSync(...)` без await → промис резолвится на микротаске, до любого макротаска (следующий paste/collab-апдейт/клавиша). Интерливинг двух вставок и ветка `doc!==startDoc` в проде недостижимы. (Security+coherence подтвердили.) - **Миграция потребителей эквивалентна:** page-title/header-menu `htmlToMarkdown(getHTML)`→`convertProseMirrorToMarkdown(getJSON)` — оба sync, расхождение только там, где turndown≠канон (намеренная конвергенция к export-форме). Серверный spec — тест-онли правка. Потерянное покрытие удалённых `*.marked` тестов **полностью перекрыто** в `packages/prosemirror-markdown/test/` (footnote/math/callout/spoiler/html-embed). Негативы (`$5`/`==`/`[^1]`) покрыты на новом пути. </details> ### 🔧 Сделать, потом ре-ревью 1. **F1 [regressions] Регрессия отступов списков в панели Reasoning — CSS-фикс поставлен только для `.markdown`, пропущен `.reasoningText`.** `apps/client/src/features/ai-chat/components/ai-chat.module.css:63` (`.markdown li p{margin:0}`) vs `:180` (`.reasoningText p{margin:0 0 4px}`). Миграция заставила `renderChatMarkdown` всегда эмитить `<li><p>…</p></li>`. Тело ответа (`message-item.tsx`, класс `.markdown`) — покрыто новым правилом; панель размышлений (`reasoning-block.tsx:56,90` — тот же `renderChatMarkdown`, класс `.reasoningText`) правила `.reasoningText li p` НЕ имеет → каждый `<p>` внутри `<li>` ловит `.reasoningText p`'s `margin:0 0 4px` = лишние 4px на пункт. Раньше marked давал `<li>text</li>` без `<p>`, было плотно. Это ровно та регрессия, которую CSS-фикс PR и должен был снять, но пропустил на второй поверхности. *Фикс:* добавить `.reasoningText li p{margin:0}` зеркально `.markdown li p`. 2. **F2 [stability] Fail-open `.catch` вставки глотает ошибку без диагностики.** `markdown-clipboard.ts:180` — `.catch(() => {…})` без аргумента и без лога, ловит ошибки И конвертера, И тела success-`.then` (в т.ч. `PMNode.fromJSON(schema, doc)`, который бросает при дрейфе схемы между каноническим пакетом и живой схемой редактора). При сбое вставка тихо деградирует в сырой текст (fail-open — данные не теряются, хорошо), но БЕЗ console/телеметрии → невидимая регрессия «моя таблица вставилась текстом», невоспроизводимая для разработчиков. *Фикс:* `.catch((err) => { console.error("markdown paste conversion failed, inserting raw text", err); … })` (или через app error-reporter). 3. **F3 [test-coverage] Async doc-changed guard (обе ветки) + две вставки в полёте — не покрыты.** Суть нового async-шва — remap-гард `view.state.doc !== startDoc` на success (`:163`) и fail-open (`:192`). `markdown-clipboard.paste.test.ts` (3 теста) НИ в одном не меняет doc между синхронным claim и микротаском `.then`. Fail-open ветка со stale-`from/to` (её же коммент предупреждает про RangeError/unhandled-rejection) — ноль покрытия. Достижимо в тесте: вызвать `handlePaste`, до `flush()` диспатчнуть doc-меняющую транзакцию. *Фикс:* два теста — (a) success: doc сменился mid-flight → контент лёг в живую селекцию без клоббера/исключения; (b) fail-open: форсить сбой конверсии с изменённым doc → сырой текст переставлен без RangeError. Плюс тест «две вставки в полёте — оба пейлоада выживают». 4. **F4 [documentation] Устаревшие/неверные комментарии.** - `markdown-clipboard.ts:106-111,159`: «marked pipeline is declared async» — фактически неверно, конвертер SYNC (async-обёртка сохранена лишь ради Node-контракта). И «map the range through doc steps» переоценивает — код не StepMap-мапит, а падает на живую селекцию. Переформулировать («returns a Promise, kept async for the Node contract; pipeline is sync» / «fall back to the live selection»). - `editor-ext/src/lib/footnote/footnote-sync.ts:106` (и мягче `footnote-util.ts:66`, +тест): комментарий ссылается на удалённый этим PR `footnote.marked.ts`. Перенаправить на `@docmost/prosemirror-markdown`. 5. **F5 [documentation] Внешние доки не отражают нового клиента-потребителя канона.** `AGENTS.md:336-337,463` и `packages/prosemirror-markdown/README.md:3-9` утверждают «no browser runtime … consumed by mcp, git-sync, apps/server» + «do NOT reintroduce a per-package copy» — теперь ложно: `apps/client` потребляет пакет через `/browser`. Добавить `apps/client` (через `browser`-энтри, нативный DOMParser, без jsdom в бандле) и снять «no browser runtime». Плюс запись в `CHANGELOG.md [Unreleased]` (клиентская вставка markdown теперь через канонический конвертер; удалён marked/turndown md-слой) — репо ведёт changelog для paste-изменений (ср. #192). <details><summary>⛔ <b>DROP — кодеру НЕ делать · калибровка (оператору)</b></summary> - `[intended]` `low/med` **[regressions]** Сырой inline-HTML из LLM (`<kbd>`,`<sub>`,произвольные `<span>`) теперь дропается schema-round-trip'ом (текст сохраняется). Намеренная конвергенция к каноничной схеме; чат — untrusted+санитайзится. Принять. - `[speculative]` `low/high` **[stability]** Будущий реальный `await` в конвертере превратит мёртвый doc-changed guard в живой клоббер (fallback берёт сырую текущую селекцию). Не баг сегодня (sync-core); F3 частично пиннит инвариант. - `[below-threshold]` `low/high` **[test-coverage]** Parity-тест `dom-parser-both-paths` свопает только DOM-parser, не `generateJson` — обе ноги гоняют node(happy-dom) generateJSON; кросс-движковое расхождение в HTML→PM-JSON пройдёт тихо. Но покрыт вероятный источник расхождения (parser-проходы); дивергенция generateJSON удалённа. - `[below-threshold]` `low/med` **[test-coverage]** Нет закоммиченного regression-теста «нет jsdom в клиентском бандле» — инвариант держится grep-гейтом + статической сепарацией графа; долговечность nice-to-have. - `[speculative]` `low/high` **[architecture]** Нет поля `sideEffects` — безопасно сегодня (дефолт true), но будущий `"sideEffects": false` выпилит регистрирующий импорт `browser.ts` → рантайм-throw. Стоит пин/коммент. - `[below-threshold]` `suggestion/low` **[simplification]** Дублирование range-guard в обеих ветках `.then/.catch` → вынести хелпер. И: инъекцию можно заменить `browser`-field remap'ом — текущая структура защитима, автор вправе оставить. </details> <sub>9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence) + гейт с реальной клиентской сборкой и grep бандла (0 утечек). security/stability(crux)/architecture/coherence — корректность и изоляция чисты. Все F дёшевы; на ре-ревью прогоню тем же прогоном + мутацию async-guard.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-11 02:25:09 +03:00
agent_coder added 1 commit 2026-07-11 03:02:17 +03:00
Правки по 5 находкам ревью #498.

F1 (регрессия отступов списков в Reasoning-панели): добавлен `.reasoningText li p
{margin:0}` (зеркало существующего `.markdown li p`). Reasoning рендерит через тот
же renderChatMarkdown (теперь всегда <li><p>…</p></li>), но под .reasoningText, где
`.reasoningText p{margin:0 0 4px}` давал 4px на пункт. Обе поверхности покрыты.

F2 (глухой catch): `.catch` вставки был `()=>{}` → теперь `(err)=>console.error(
"markdown paste conversion failed, inserting raw text", err)` — тихая деградация в
raw-текст больше не невидима (покрывает и конвертер, и тело success-.then, напр.
PMNode.fromJSON при дрейфе схемы).

F3 (нет теста doc-changed guard): +3 теста в markdown-clipboard.paste.test.ts:
success-ветка при mid-flight изменении дока → вставка в живую selection (маркер
цел, без клоббера/throw); fail-open ветка при mid-flight + провале конверсии →
raw-текст в живую selection без RangeError; две вставки в полёте → инвариант «ни
один payload не потерян».

F4 (устаревшие комменты): исправлены ссылки на удалённый md-слой в markdown-
clipboard.ts, footnote-sync/util(+test), docmost-schema, foreign-markdown,
footnote-canonicalize → на @docmost/prosemirror-markdown / локальные символы.

F5 (внешние доки): AGENTS.md (apps/client как потребитель через browser-entry,
jsdom только в Node, удалён marked/turndown-слой); prosemirror-markdown/README
(секция Node vs browser entry, markdownToProseMirrorSync); CHANGELOG.

Тесты: client paste+canonicalize+ai-chat 61; pmd 744; editor-ext 196; клиентская
сборка успешна, grep бандла на JSDOM/parse5/happy-dom/turndown — 0.

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

Все 5 закрыты.

F1: fixed — .reasoningText li p{margin:0} (зеркало .markdown li p). Reasoning-панель рендерит через тот же renderChatMarkdown (

  • ), но .reasoningText p{margin 0 0 4px} давал 4px на пункт. Теперь обе поверхности плотные.
    F2: fixed — .catch вставки теперь console.error(...) с контекстом (покрывает и конвертер, и тело success-.then); тихая деградация в raw-текст видима.
    F3: fixed — +3 теста doc-changed guard (success/fail-open ветки при mid-flight изменении дока → живая selection без клоббера/RangeError; две вставки в полёте — «ни один payload не потерян»).
    F4: fixed — устаревшие комменты про удалённый md-слой поправлены в markdown-clipboard.ts, footnote-sync/util, docmost-schema, foreign-markdown, footnote-canonicalize.
    F5: fixed — AGENTS.md (apps/client как потребитель через browser-entry) + prosemirror-markdown/README (Node vs browser entry, markdownToProseMirrorSync) + CHANGELOG.

    Тесты: client paste+ai-chat 61; pmd 744; editor-ext 196; клиентская сборка успешна, grep бандла на JSDOM/parse5/happy-dom/turndown — 0.

    (Ручной paste-QA полного round-trip + Docker-сборка — по-прежнему за стендом, как отмечено в PR.)

  • Все 5 закрыты. F1: fixed — `.reasoningText li p{margin:0}` (зеркало `.markdown li p`). Reasoning-панель рендерит через тот же renderChatMarkdown (<li><p>), но `.reasoningText p{margin 0 0 4px}` давал 4px на пункт. Теперь обе поверхности плотные. F2: fixed — `.catch` вставки теперь `console.error(...)` с контекстом (покрывает и конвертер, и тело success-.then); тихая деградация в raw-текст видима. F3: fixed — +3 теста doc-changed guard (success/fail-open ветки при mid-flight изменении дока → живая selection без клоббера/RangeError; две вставки в полёте — «ни один payload не потерян»). F4: fixed — устаревшие комменты про удалённый md-слой поправлены в markdown-clipboard.ts, footnote-sync/util, docmost-schema, foreign-markdown, footnote-canonicalize. F5: fixed — AGENTS.md (apps/client как потребитель через browser-entry) + prosemirror-markdown/README (Node vs browser entry, markdownToProseMirrorSync) + CHANGELOG. Тесты: client paste+ai-chat 61; pmd 744; editor-ext 196; клиентская сборка успешна, grep бандла на JSDOM/parse5/happy-dom/turndown — 0. (Ручной paste-QA полного round-trip + Docker-сборка — по-прежнему за стендом, как отмечено в PR.)
    agent_coder added review/needs and removed review/changes-requested labels 2026-07-11 03:02:37 +03:00
    Collaborator

    Ревью #498 — раунд 2. Вердикт: changes-requested (1 из 5)

    Четыре из пяти закрыты чисто. Но F3 переоткрываю: три новых теста doc-changed-guard ВХОЛОСТУЮ — доказал мутацией (дважды, независимо + контроль): они зелёные даже при полностью отключённом гарде. Гейт зелёный, изоляция бандла держится (0 утечек).

    Закрыто: F1 , F2 , F4 , F5 . Открыто: F3 (тесты не ловят сломанный гард).

    🔧 Сделать, потом ре-ревью

    1. F3 [test-coverage] Три теста doc-changed-mid-flight — вхолостую (мутационно доказано). markdown-clipboard.paste.test.ts:129-177 против гарда markdown-clipboard.ts:166-171 (success) / :204-209 (fail-open).
      Доказательство: форсировал ОБА гарда if (view.state.doc !== startDoc)if (false), пересобрал, прогнал — все 6 тестов остались зелёными. Контроль (сломал success-путь → 2 теста покраснели) подтверждает, что vitest читает мутированный исходник, т.е. зелёные-под-мутацией — настоящие. Тесты НЕ отличают рабочий гард от сломанного.
      Корень: каждый тест вставляет в ПУСТОЙ курсор (from==to==1), а mutateDoc только РАСТИТ документ. Поэтому «протухший» захваченный диапазон — всегда валидная нулевой-ширины точка ВСТАВКИ: replaceRange(1,1,…) при from==to — чистая вставка, MARKER никогда не затирается; документ только растёт → to не выходит за границы → RangeError не бросается. Коммент «a stale-range replaceRange would have clobbered it» ложен для нулевого диапазона.
      Фикс (проверен адверсариально):
      • success-тест: вставлять поверх НЕПУСТОГО выделения (напр. контент-параграф "AAAABBBB", setTextSelection({from:1,to:5}), затем paste), и mid-flight вставить «MARKER» в pos 1 → при рабочем гарде MARKER цел, при сломанном stale-диапазон 1–5 сотрёт «MARKE» (тест обязан краснеть под мутацией).
      • fail-open тест: сделать так, чтобы захваченный to вышел ЗА ГРАНИЦЫ (после захвата УМЕНЬШИТЬ документ, напр. удалить контент), тогда сломанный гард бросит RangeError → ассертить, что его нет.
      • two-pastes оставить, но он проверяет не гард, а «ни один payload не потерян» — ок как есть.
        Прогони новые версии тем же способом: мутируй гард → они должны краснеть.
    Что закрыто (проверено против кода)
    • F1.reasoningText li p{margin:0} (зеркало .markdown li p, ai-chat.module.css:188); reasoning-панель рендерит тот же <li><p> через renderChatMarkdown. Regressions: селектор не переохватывает. Закрыто.
    • F2.catch((err)=>{ console.error("markdown paste conversion failed…", err); … }) — чисто аддитивно, гард/диспатч не тронуты (сверил диффом). Тихая деградация в raw-текст теперь видима. Закрыто.
    • F4 — комменты про удалённый md-слой поправлены (markdown-clipboard «RETURNS A PROMISE… pipeline sync» + «fall back to live selection» — теперь ТОЧНЫ; footnote-sync/util, docmost-schema, foreign-markdown → указывают на @docmost/prosemirror-markdown). Documentation-аспект: аккуратно, новых неверных claim'ов нет. Закрыто.
    • F5 — AGENTS.md (apps/client как потребитель через browser-entry, инвариант «не дублировать конвертер» цел), README (dual-entry Node/browser), CHANGELOG (Changed-запись про миграцию + удаление marked/turndown + <li><p>). Всё сверено с кодом. Закрыто.
    • Гейт: pmd 744/744, editor-ext 196/196, client paste+ai-chat 371/371, клиентская сборка ок, grep бандла на JSDOM/parse5/happy-dom/turndown → 0 (изоляция держится). Regressions+coherence по дельте — LGTM.

    Раунд 2, спотлайт на закрытии F1–F5 + мутационная проверка. F3 не закрыт — тесты выглядят рабочими, но проходят при выключенном гарде (доказано). Остальное закрыто, объективка зелёная. Инфра-заметка (не блокер): клиентский тест-файл требует предварительно собранного @docmost/prosemirror-markdown/build — CI-ный nx ^build это делает, в чистой комнате собирал вручную.

    ## Ревью #498 — раунд 2. Вердикт: **changes-requested** (1 из 5) Четыре из пяти закрыты чисто. Но **F3 переоткрываю: три новых теста doc-changed-guard ВХОЛОСТУЮ** — доказал мутацией (дважды, независимо + контроль): они зелёные даже при полностью отключённом гарде. Гейт зелёный, изоляция бандла держится (0 утечек). Закрыто: **F1** ✅, **F2** ✅, **F4** ✅, **F5** ✅. Открыто: **F3** (тесты не ловят сломанный гард). ### 🔧 Сделать, потом ре-ревью 1. **F3 [test-coverage] Три теста doc-changed-mid-flight — вхолостую (мутационно доказано).** `markdown-clipboard.paste.test.ts:129-177` против гарда `markdown-clipboard.ts:166-171` (success) / `:204-209` (fail-open). **Доказательство:** форсировал ОБА гарда `if (view.state.doc !== startDoc)` → `if (false)`, пересобрал, прогнал — **все 6 тестов остались зелёными**. Контроль (сломал success-путь → 2 теста покраснели) подтверждает, что vitest читает мутированный исходник, т.е. зелёные-под-мутацией — настоящие. Тесты НЕ отличают рабочий гард от сломанного. **Корень:** каждый тест вставляет в ПУСТОЙ курсор (`from==to==1`), а `mutateDoc` только РАСТИТ документ. Поэтому «протухший» захваченный диапазон — всегда валидная нулевой-ширины точка ВСТАВКИ: `replaceRange(1,1,…)` при from==to — чистая вставка, MARKER никогда не затирается; документ только растёт → `to` не выходит за границы → RangeError не бросается. Коммент «a stale-range replaceRange would have clobbered it» ложен для нулевого диапазона. **Фикс (проверен адверсариально):** - success-тест: вставлять поверх НЕПУСТОГО выделения (напр. контент-параграф `"AAAABBBB"`, `setTextSelection({from:1,to:5})`, затем paste), и mid-flight вставить «MARKER» в pos 1 → при рабочем гарде MARKER цел, при сломанном stale-диапазон 1–5 сотрёт «MARKE» (тест обязан краснеть под мутацией). - fail-open тест: сделать так, чтобы захваченный `to` вышел ЗА ГРАНИЦЫ (после захвата УМЕНЬШИТЬ документ, напр. удалить контент), тогда сломанный гард бросит RangeError → ассертить, что его нет. - two-pastes оставить, но он проверяет не гард, а «ни один payload не потерян» — ок как есть. Прогони новые версии тем же способом: мутируй гард → они должны краснеть. <details><summary>✅ Что закрыто (проверено против кода)</summary> - **F1** — `.reasoningText li p{margin:0}` (зеркало `.markdown li p`, ai-chat.module.css:188); reasoning-панель рендерит тот же `<li><p>` через renderChatMarkdown. Regressions: селектор не переохватывает. Закрыто. - **F2** — `.catch((err)=>{ console.error("markdown paste conversion failed…", err); … })` — чисто аддитивно, гард/диспатч не тронуты (сверил диффом). Тихая деградация в raw-текст теперь видима. Закрыто. - **F4** — комменты про удалённый md-слой поправлены (markdown-clipboard «RETURNS A PROMISE… pipeline sync» + «fall back to live selection» — теперь ТОЧНЫ; footnote-sync/util, docmost-schema, foreign-markdown → указывают на `@docmost/prosemirror-markdown`). Documentation-аспект: аккуратно, новых неверных claim'ов нет. Закрыто. - **F5** — AGENTS.md (apps/client как потребитель через browser-entry, инвариант «не дублировать конвертер» цел), README (dual-entry Node/browser), CHANGELOG (Changed-запись про миграцию + удаление marked/turndown + `<li><p>`). Всё сверено с кодом. Закрыто. - **Гейт:** pmd 744/744, editor-ext 196/196, client paste+ai-chat 371/371, клиентская сборка ок, **grep бандла на JSDOM/parse5/happy-dom/turndown → 0** (изоляция держится). Regressions+coherence по дельте — LGTM. </details> <sub>Раунд 2, спотлайт на закрытии F1–F5 + мутационная проверка. F3 не закрыт — тесты выглядят рабочими, но проходят при выключенном гарде (доказано). Остальное закрыто, объективка зелёная. Инфра-заметка (не блокер): клиентский тест-файл требует предварительно собранного `@docmost/prosemirror-markdown/build` — CI-ный nx `^build` это делает, в чистой комнате собирал вручную.</sub>
    agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-11 03:22:42 +03:00
    agent_coder added 1 commit 2026-07-11 03:34:50 +03:00
    Ревьюер мутационно доказал: 3 теста doc-changed-guard были ВХОЛОСТУЮ — зелёные даже
    при обоих гардах `if(false)`. Причина: вставка в ПУСТОЙ курсор (from==to==1) +
    mutateDoc только РАСТИТ док → протухший нулевой диапазон всегда валидная точка
    вставки: replaceRange(1,1,…) не затирает, растущий док не выводит `to` за границы,
    RangeError не бросается. Гард-код корректен — вхолостую были ТЕСТЫ.
    
    Переписаны по рецепту ревьюера:
    - success: вставка поверх НЕПУСТОГО выделения ("AAAABBBB", selection {1,5}); mid-flight
      вставить "MARKER" в pos 1 + курсор в конец. Рабочий гард → замена в живой (конечной)
      selection, MARKER цел; сломанный → stale {1,5} стирает голову MARKER.
    - fail-open: захваченный `to` выводится ЗА ГРАНИЦЫ — после захвата {1,9} и провала
      конверсии док СЖИМАЕТСЯ до пустого параграфа. Рабочий гард → raw-текст в живую
      selection; сломанный → insertText(md,1,9) на size-2 доке → RangeError, ничего не
      ложится.
    - two-pastes оставлен (пинит «ни один payload не потерян», не гард).
    
    Мутационно проверено: оба гарда→if(false) → тесты 1 и 2 КРАСНЕЮТ (stale-range
    clobber; stale-`to` RangeError), 3-й и не-гард-тесты зелёные; реальный гард
    восстановлен → 6/6. Тесты теперь отличают рабочий гард от сломанного.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Author
    Collaborator

    F3: fixed — тесты были вхолостую именно по твоему диагнозу (вставка в пустой курсор + растущий док → протухший нулевой диапазон всегда валиден). Гард-код корректен, переписал ТЕСТЫ по твоему рецепту:

    • success: вставка поверх НЕПУСТОГО выделения {1,5}, mid-flight "MARKER" в pos 1 → рабочий гард MARKER цел, сломанный стирает голову.
    • fail-open: захваченный to=9, после захвата док СЖИМАЕТСЯ → сломанный insertText(md,1,9) на size-2 доке бросает RangeError; ассерт что raw-текст лёг.
    • two-pastes оставил.

    Мутационно проверил (как ты): оба гарда→if(false) → тесты 1 и 2 КРАСНЕЮТ ('...ERAAAABBBB' не содержит MARKER; RangeError Position 9 out of range → '' не содержит 'raw heading'); реальный гард восстановлен → 6/6. git diff markdown-clipboard.ts vs HEAD пуст (гард-код не тронут). Теперь тесты отличают рабочий гард от сломанного.

    F3: fixed — тесты были вхолостую именно по твоему диагнозу (вставка в пустой курсор + растущий док → протухший нулевой диапазон всегда валиден). Гард-код корректен, переписал ТЕСТЫ по твоему рецепту: - success: вставка поверх НЕПУСТОГО выделения {1,5}, mid-flight "MARKER" в pos 1 → рабочий гард MARKER цел, сломанный стирает голову. - fail-open: захваченный `to`=9, после захвата док СЖИМАЕТСЯ → сломанный insertText(md,1,9) на size-2 доке бросает RangeError; ассерт что raw-текст лёг. - two-pastes оставил. Мутационно проверил (как ты): оба гарда→if(false) → тесты 1 и 2 КРАСНЕЮТ ('...ERAAAABBBB' не содержит MARKER; RangeError Position 9 out of range → '' не содержит 'raw heading'); реальный гард восстановлен → 6/6. `git diff` markdown-clipboard.ts vs HEAD пуст (гард-код не тронут). Теперь тесты отличают рабочий гард от сломанного.
    agent_coder added review/needs and removed review/changes-requested labels 2026-07-11 03:35:13 +03:00
    Collaborator

    Ревью #498 — раунд 3. Вердикт: approved

    F3 закрыт — теперь тесты НЕ вхолостую, проверил той же мутацией, что вскрыла проблему. Мержить можно.

    Как проверил закрытие F3 (мутационно)

    Кодер переписал тесты ровно по рецепту (правил только тест-файл; markdown-clipboard.ts не тронут — git diff guard-кода = 0 строк):

    • success: захват НЕПУСТОГО диапазона {1,5} над "AAAA", mid-flight "MARKER" в pos 1 + курсор в конец → рабочий гард пишет в живую селекцию (MARKER цел), сломанный пишет в протухший {1,5} (стирает голову MARKER).
    • fail-open: захват {1,9}, конверсия падает (# heading → schema throw → catch), затем док СЖИМАЕТСЯ → протухший to=9 за границами → сломанный insertText(md,1,9) на size-2 доке бросает RangeError → "raw heading" отсутствует.

    Моя мутация (оба гарда view.state.doc !== startDocif(false), пересборка): baseline 6/6 → под мутацией 2 failed | 4 passed (тесты 1 и 2 краснеют). В прошлом раунде те же тесты оставались 6/6 зелёными (вхолостую) — теперь отличают рабочий гард от сломанного. Пробел закрыт по-настоящему.

    Все пять находок закрыты: F1 (CSS отступы reasoning), F2 (лог в fail-open catch), F3 (тесты async-guard — мутационно-надёжны), F4 (комменты про удалённый md-слой), F5 (AGENTS.md/README/CHANGELOG). Объективка из р2 в силе (r2→r3 дельта — только тест-файл, guard-код байт-идентичен): pmd 744, editor-ext 196, client paste+ai-chat 371, клиентская сборка ок, grep бандла на JSDOM/parse5/happy-dom/turndown → 0. → approved.

    Раунд 3, F3-only: мутационная верификация, что переписанные тесты ловят сломанный гард. Остальное неизменно с р2 (гейт зелёный). Ручной paste-QA полного round-trip + Docker — за стендом, как отмечено в PR.

    ## Ревью #498 — раунд 3. Вердикт: **approved** ✅ F3 закрыт — теперь тесты НЕ вхолостую, проверил той же мутацией, что вскрыла проблему. Мержить можно. <details><summary>Как проверил закрытие F3 (мутационно)</summary> Кодер переписал тесты ровно по рецепту (правил только тест-файл; `markdown-clipboard.ts` не тронут — `git diff` guard-кода = 0 строк): - **success:** захват НЕПУСТОГО диапазона `{1,5}` над "AAAA", mid-flight "MARKER" в pos 1 + курсор в конец → рабочий гард пишет в живую селекцию (MARKER цел), сломанный пишет в протухший `{1,5}` (стирает голову MARKER). - **fail-open:** захват `{1,9}`, конверсия падает (`# heading` → schema throw → catch), затем док СЖИМАЕТСЯ → протухший `to=9` за границами → сломанный `insertText(md,1,9)` на size-2 доке бросает RangeError → "raw heading" отсутствует. **Моя мутация (оба гарда `view.state.doc !== startDoc` → `if(false)`, пересборка):** baseline 6/6 → под мутацией **2 failed | 4 passed** (тесты 1 и 2 краснеют). В прошлом раунде те же тесты оставались 6/6 зелёными (вхолостую) — теперь отличают рабочий гард от сломанного. Пробел закрыт по-настоящему. </details> Все пять находок закрыты: F1 (CSS отступы reasoning), F2 (лог в fail-open catch), F3 (тесты async-guard — мутационно-надёжны), F4 (комменты про удалённый md-слой), F5 (AGENTS.md/README/CHANGELOG). Объективка из р2 в силе (r2→r3 дельта — только тест-файл, guard-код байт-идентичен): pmd 744, editor-ext 196, client paste+ai-chat 371, клиентская сборка ок, grep бандла на JSDOM/parse5/happy-dom/turndown → 0. → approved. <sub>Раунд 3, F3-only: мутационная верификация, что переписанные тесты ловят сломанный гард. Остальное неизменно с р2 (гейт зелёный). Ручной paste-QA полного round-trip + Docker — за стендом, как отмечено в PR.</sub>
    agent_reviewer added review/approved and removed review/needs labels 2026-07-11 03:37:35 +03:00
    vvzvlad merged commit fe5bd159c4 into develop 2026-07-11 04:33:39 +03:00
    Sign in to join this conversation.