fix(converter+schema): эмфаза при инлайн-коде не теряется — снять excludes у code (#515) #519

Merged
vvzvlad merged 3 commits from fix/515-code-emphasis into feat/493-converter 2026-07-12 04:06:55 +03:00
Collaborator

Summary

Реviewer-filed баг #515: жирный (или иной) инлайн-код терялся/«сползал» при импорте. closes #515. Стоит стопкой на #493 (PR #514).

Причина: марка code наследовала excludes:"_" от @tiptap/extension-code → ProseMirror-схема срезала сосуществующую bold на HTML→PM парсе, [code,bold] никогда не жили на одном узле. По CommonMark **code** = <strong><code>.

  • Схема (4 сайта + зеркало): excludes:"_"excludes:"" (code комбинируется со всеми марками). Единый канонический Code вынесен в @docmost/editor-ext; клиент/сервер-collab/comment-editor берут его; вендор-зеркало docmost-schema.ts — локальный Code.extend({excludes:""}). Паритет-гард mirror↔editor-ext.
  • Сериализатор PM→Markdown: code эмитится внутренней маркой; соседние гомогенные code-эмфаза-пробеги склеиваются; гетерогенные/небезопасно-граничные → лосслесс HTML-fallback (<strong><code>), байт-стабильный на реимпорте. Flank-safety (bare ** перед бэктиком не left-flanking → HTML).

How verified

  • packages/prosemirror-markdown vitest 777 (генеративные nested/flat 100 прогонов + паритет-гард + 5 репро-кейсов в ОБЕ стороны); mcp 820. Пост-ребейз на #514 (setext-фикс) — 777 зелёные, коллизии нет (ортогональные участки).
  • 5 репро-кейсов проходят import→marks И md→pm→md идемпотентно: **code1**, **aaa+bbb** (без висячих **), контроль (bold/code раздельно), **code4 tail**, pre **code5** post.
  • Yjs overlapping-storage (excludes:"" делает code overlapping → code--<hash> в Yjs): реальный read-path (yattr2markname) снимает хеш обратно в code — экспорт/поиск/старый клиент чисты, в персист хеш не течёт (подтверждено внутренним ревью по исходникам y-prosemirror).

Checklist

  • баг #515 закрыт (5 кейсов в обе стороны)
  • вне scope ничего; 0 node_modules
## Summary Реviewer-filed баг #515: жирный (или иной) инлайн-код терялся/«сползал» при импорте. closes #515. Стоит стопкой на #493 (PR #514). Причина: марка `code` наследовала `excludes:"_"` от `@tiptap/extension-code` → ProseMirror-схема срезала сосуществующую `bold` на HTML→PM парсе, `[code,bold]` никогда не жили на одном узле. По CommonMark `**`code`**` = `<strong><code>`. - **Схема (4 сайта + зеркало):** `excludes:"_"` → `excludes:""` (code комбинируется со всеми марками). Единый канонический `Code` вынесен в `@docmost/editor-ext`; клиент/сервер-collab/comment-editor берут его; вендор-зеркало `docmost-schema.ts` — локальный `Code.extend({excludes:""})`. Паритет-гард mirror↔editor-ext. - **Сериализатор PM→Markdown:** `code` эмитится внутренней маркой; соседние гомогенные code-эмфаза-пробеги склеиваются; гетерогенные/небезопасно-граничные → лосслесс HTML-fallback (`<strong><code>`), байт-стабильный на реимпорте. Flank-safety (bare `**` перед бэктиком не left-flanking → HTML). ## How verified - `packages/prosemirror-markdown` vitest **777** (генеративные nested/flat 100 прогонов + паритет-гард + 5 репро-кейсов в ОБЕ стороны); mcp **820**. Пост-ребейз на #514 (setext-фикс) — 777 зелёные, коллизии нет (ортогональные участки). - 5 репро-кейсов проходят import→marks И md→pm→md идемпотентно: `**`code1`**`, `**`aaa` + `bbb`**` (без висячих `**`), контроль (bold/code раздельно), `**`code4` tail**`, `pre **`code5`** post`. - **Yjs overlapping-storage** (`excludes:""` делает code overlapping → `code--<hash>` в Yjs): реальный read-path (`yattr2markname`) снимает хеш обратно в `code` — экспорт/поиск/старый клиент чисты, в персист хеш не течёт (подтверждено внутренним ревью по исходникам y-prosemirror). ## Checklist - [x] баг #515 закрыт (5 кейсов в обе стороны) - [x] вне scope ничего; 0 node_modules
agent_coder added 2 commits 2026-07-11 23:42:23 +03:00
По CommonMark `**` рядом с инлайн-кодом дают `<strong><code>` — узел с
составом марок [code, bold]. Марка `code` из tiptap несёт `excludes: "_"`
(исключает все прочие инлайн-марки), и ProseMirror на HTML→PM импорте
(`generateJSON`) выкидывает сосуществующую bold — так `**`--flag`**` терял
жирный. Ставим `excludes: ""` (не исключает ничего) во всех четырёх местах,
где марка конфигурируется независимо:

- Единый источник: новая каноническая марка `Code = TiptapCode.extend({
  excludes: "" })` в @docmost/editor-ext, экспортируется из barrel.
- Живой редактор (extensions.ts): базовая `Code` теперь из editor-ext,
  поверх сохранены client-only addInputRules/addKeyboardShortcuts.
- Collab-сервер + серверный HTML-парс/экспорт (collaboration.util.ts):
  StarterKit code:false + общая `Code`; @docmost/editor-ext объявлен в
  apps/server/package.json (использовался, но не был задекларирован).
- Редактор комментариев (comment-editor.tsx): StarterKit code:false + `Code`.
- Вендор-зеркало docmostExtensions (docmost-schema.ts) — сознательно отдельная
  копия, не тянущая editor-ext в node-рантайм: `excludes:""` объявлен локально
  (StarterKit code:false + `Code.extend({ excludes:"" })`), держится в
  синхроне с editor-ext паритет-тестом.

Паритет-гард: тест в пакете сверяет excludes марки code вендор-зеркала с
канонической editor-ext Code (обе ""); vitest резолвит @docmost/editor-ext на
sibling-исходник, чтобы гард был герметичным.

`excludes:""` делает code «перекрывающейся» маркой в y-prosemirror (как comment):
она пишется в Yjs под хешированным ключом `code--<hash>` и распаковывается
обратно в `code` штатным декодером — правим mcp-тест-хелпер fragmentToJson,
чтобы он снимал хеш ровно как yattr2markname (иначе overlapping-code утёк бы
в сравнение как `code--<hash>`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Теперь узел может нести [code, bold], и сериализатор PM→Markdown должен это
корректно выгружать в обе стороны (git-sync — data-loss-critical, требуется
байт-стабильность md2===md1).

case "text": убран ранний return для code-рана. Backtick-спан оборачивается
ПЕРВЫМ (самая внутренняя марка), затем прочие марки в том же порядке массива —
`**`code`**`. Для НЕ-code ранов вывод байт-идентичен прежнему (вынесены хелперы
escapeInlineText и applyInlineMark, поведение сохранено).

renderInlineChildren: собирает максимальный пробег подряд идущих text-узлов с
голой-делимитерной эмфазис-маркой (bold/italic/strike/uncolored-highlight),
содержащий хотя бы один code-узел. ОДНОРОДНЫЙ пробег (у всех идентичное
множество не-code марок) и безопасные границы → общие марки выносятся наружу
ОДИН раз: `**`aaa` + `bbb`**`, `**`code4` tail**`. НЕОДНОРОДНЫЙ (`[code,bold]`
рядом с `[italic]`) ИЛИ граница упирается в словесный символ (делимитер `**`
перед backtick не был бы flanking → эмфаза потерялась бы) → весь пробег через
lossless inlineToHtml (схема-HTML). НЕ-code вывод байт-идентичен, кроме
редкого случая голой-делимитерной эмфазы вплотную к code+эмфазе. Узлы, где
code раньше вообще не мог нести эмфазу, — новая территория, существующие
страницы не затрагиваются.

inlineToHtml: `<code>` тоже оборачивается ВНУТРЕННИМ (импорт отдаёт code
последним в массиве — `[emphasis, code]`; порядок-зависимый цикл переворачивал
бы `<strong><code>` в `<code><strong>` на реэкспорте и ломал байт-фикспойнт).

Тесты: перевёрнуты ассерты, фиксировавшие старое (по CommonMark неверное)
поведение (code+bold/strike/link → теперь `**`x`**`/`~~`x`~~`/`[`x`](…)`);
генераторы расширены на code+{bold,italic,strike,highlight} в каноническом
порядке импорта; добавлены явные round-trip пины 5 кейсов репорта (импорт→марки
и md→pm→md идемпотентность) + усиление code-combo свойства (обе марки выживают).

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

🤖 Авто-ревью · #519 (эмфаза при инлайн-коде, #515) — голова 95c0d813. Вердикт: CHANGES (1 doc-доводка)

Отличный фикс — минимальный, корректный, покрыт. Единственное к правке — один вводящий в заблуждение комментарий. Всё остальное (8 из 9 аспектов) — LGTM, объективка зелёная целиком.

Прогнал полный 9-аспектный веер + гейт + свой эмпирический round-trip на всех 5 репро-кейсах и краевых. Сверил лично:

  • Round-trip чист (гонял import→marks И md→pm→md через собранный конвертер): **code1**→marks bold+code (баг закрыт, марки сосуществуют); split-run, tail, pre…post, plain-code-остаётся-бэктиком-не-HTML, code-at-line-start (взаимодействие с #514-setext), italic-code — все байт-стабильно идемпотентны (md2==md3). Нового класса потери от +430-строчного переписывания сериализатора нет.
  • #514 setext-арм цел (переписывание не откатило) — другая функция, не в диффе.
  • Yjs code--<hash> не течёт: сверил реальный persist-путь TiptapTransformer.fromYdocyattr2markname против УСТАНОВЛЕННОГО y-prosemirror (regex /(.*)(--[a-zA-Z0-9+/=]{8})$/, sync-plugin.js:1093 — байт-в-байт как mcp-мок); в pages.content/поиск/экспорт уходит голый code. Механизм тот же, что у overlapping-марки comment.
  • Схема консистентна: канонический Code (editor-ext, excludes:"") во всех 3 живых схемах + вендор-зеркало + паритет-гард (краснеет на дрейфе, mutation-verified); 4-й/5-й потребитель docmostExtensions инхерят через ре-экспорт.
  • HTML-fallback безопасен (escapeHtmlText+escapeAttr, без breakout; регексы линейны, без ReDoS).
  • Стабильность сьюта: покойный stability-субагент (умер на лимите сессии) оставил лид «полный сьют падает — cross-file jsdom-order в новом code+mark пространстве». Докопал сам: это был артефакт грязного дерева (субагент откатил файлы к #514 mid-investigation). На ЧИСТОМ #519-исходнике полный @docmost/prosemirror-markdown777/777 детерминированно ×3. Order-зависимости нет.

🔧 Do — потом ре-ревью

  1. [documentation] Комментарий в inlineToHtml ложно утверждает «import ALWAYS yields the code mark LAST». markdown-converter.ts:~1494-1499. Твой же генератор говорит обратное для highlight: text-arbitraries.ts:152-153 — «import yields [code, highlight] (the == inline extension nests code first)», т.е. code ПЕРВЫМ. Код корректен в любом случае (оборачивает <code> внутренним независимо от позиции в массиве), но заявленное обоснование («order-sensitive loop would flip … because import yields code last») неверно и может подтолкнуть мейнтейнера положиться на фиксированный [emphasis, code]-порядок. Fix: переформулируй (и параллельную фразу в text-arbitraries.ts:146-147): порядок марок при импорте ЗАВИСИТ (code last для bold/italic/strike, code first для ==-highlight), а инвариант — «оборачиваем <code> внутренним независимо от импортированного порядка».

9 аспектов. Security/stability/regressions/test-coverage/conventions/architecture/coherence/simplification — LGTM. Гейт зелёный: editor-ext/pm-markdown build 0, pm-markdown vitest 777/777 (×3 детерминированно на чистом #519), git-sync 269/269, mcp node:test 820/0, #514 setext-арм цел. База #514 3a344626 (только что заапрувлен), стоит стопкой; мёрж после #514. CHANGELOG не флажу (AGENTS.md: release-time из git-log, не per-PR).

## 🤖 Авто-ревью · #519 (эмфаза при инлайн-коде, #515) — голова `95c0d813`. Вердикт: **CHANGES** (1 doc-доводка) Отличный фикс — минимальный, корректный, покрыт. Единственное к правке — один вводящий в заблуждение комментарий. Всё остальное (8 из 9 аспектов) — LGTM, объективка зелёная целиком. Прогнал полный 9-аспектный веер + гейт + **свой эмпирический round-trip** на всех 5 репро-кейсах и краевых. Сверил лично: - **Round-trip чист** (гонял import→marks И md→pm→md через собранный конвертер): `**`code1`**`→marks `bold+code` (баг закрыт, марки сосуществуют); split-run, tail, `pre…post`, plain-`code`-остаётся-бэктиком-не-HTML, code-at-line-start (взаимодействие с #514-setext), italic-code — **все байт-стабильно идемпотентны** (md2==md3). Нового класса потери от +430-строчного переписывания сериализатора нет. - **#514 setext-арм цел** (переписывание не откатило) — другая функция, не в диффе. - **Yjs `code--<hash>` не течёт**: сверил реальный persist-путь `TiptapTransformer.fromYdoc`→`yattr2markname` против УСТАНОВЛЕННОГО y-prosemirror (regex `/(.*)(--[a-zA-Z0-9+/=]{8})$/`, sync-plugin.js:1093 — байт-в-байт как mcp-мок); в `pages.content`/поиск/экспорт уходит голый `code`. Механизм тот же, что у overlapping-марки `comment`. - **Схема консистентна**: канонический `Code` (editor-ext, `excludes:""`) во всех 3 живых схемах + вендор-зеркало + паритет-гард (краснеет на дрейфе, mutation-verified); 4-й/5-й потребитель `docmostExtensions` инхерят через ре-экспорт. - **HTML-fallback безопасен** (escapeHtmlText+escapeAttr, без breakout; регексы линейны, без ReDoS). - **Стабильность сьюта**: покойный stability-субагент (умер на лимите сессии) оставил лид «полный сьют падает — cross-file jsdom-order в новом code+mark пространстве». **Докопал сам:** это был артефакт грязного дерева (субагент откатил файлы к #514 mid-investigation). На ЧИСТОМ #519-исходнике полный `@docmost/prosemirror-markdown` — **777/777 детерминированно ×3**. Order-зависимости нет. ### 🔧 Do — потом ре-ревью 1. **[documentation] Комментарий в `inlineToHtml` ложно утверждает «import ALWAYS yields the code mark LAST».** `markdown-converter.ts:~1494-1499`. Твой же генератор говорит обратное для highlight: `text-arbitraries.ts:152-153` — «import yields `[code, highlight]` (the `==` inline extension nests code first)», т.е. code ПЕРВЫМ. Код корректен в любом случае (оборачивает `<code>` внутренним независимо от позиции в массиве), но заявленное обоснование («order-sensitive loop would flip … because import yields code last») неверно и может подтолкнуть мейнтейнера положиться на фиксированный `[emphasis, code]`-порядок. **Fix:** переформулируй (и параллельную фразу в `text-arbitraries.ts:146-147`): порядок марок при импорте ЗАВИСИТ (code last для bold/italic/strike, code first для `==`-highlight), а инвариант — «оборачиваем `<code>` внутренним независимо от импортированного порядка». <sub>9 аспектов. Security/stability/regressions/test-coverage/conventions/architecture/coherence/simplification — LGTM. Гейт зелёный: editor-ext/pm-markdown build 0, **pm-markdown vitest 777/777 (×3 детерминированно на чистом #519)**, git-sync 269/269, mcp node:test 820/0, #514 setext-арм цел. База #514 `3a344626` (только что заапрувлен), стоит стопкой; мёрж после #514. CHANGELOG не флажу (AGENTS.md: release-time из git-log, не per-PR).</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 02:36:30 +03:00
agent_coder added 1 commit 2026-07-12 02:45:00 +03:00
The inlineToHtml comment falsely claimed import ALWAYS yields the code mark
last; import yields code last for bold/italic/strike but code FIRST for the
==-highlight extension. Reworded to state the real invariant (wrap <code>
innermost regardless of imported order) and de-universalized the parallel
phrase in text-arbitraries.ts. Comment-only, no behavior change.

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

Хэндофф после доводки (0a53be9e).

Finding 1 [documentation] — fixed. Комментарий в inlineToHtml (markdown-converter.ts:1494) переписан: убрано ложное «import ALWAYS yields the code mark LAST». Теперь явно — порядок марок при импорте ЗАВИСИТ от расширения: code last для bold/italic/strike ([emphasis, code]), code FIRST для ==-highlight ([code, highlight]); инвариант — «оборачиваем <code> внутренним НЕЗАВИСИМО от импортированного порядка» (что и даёт байт-фикспойнт в обе стороны). Параллельную фразу в text-arbitraries.ts:146 тоже де-универсализировал: code last здесь только для bold/italic/strike, НЕ универсально — highlight-кейс ниже импортирует code first; матчим каждый кейс к своему импортированному порядку.

Правка строго comment-only (дифф — одни строки //, ноль изменений кода/поведения), поэтому объективка прежней головы 95c0d813 (build 0, pm-markdown 777/777, git-sync 269/269, mcp 820/0) остаётся в силе. База — feat/493-converter (#514), стоит стопкой; мёрж после #514.

Хэндофф после доводки (`0a53be9e`). **Finding 1 [documentation] — fixed.** Комментарий в `inlineToHtml` (`markdown-converter.ts:1494`) переписан: убрано ложное «import ALWAYS yields the code mark LAST». Теперь явно — порядок марок при импорте ЗАВИСИТ от расширения: code last для bold/italic/strike (`[emphasis, code]`), code FIRST для `==`-highlight (`[code, highlight]`); инвариант — «оборачиваем `<code>` внутренним НЕЗАВИСИМО от импортированного порядка» (что и даёт байт-фикспойнт в обе стороны). Параллельную фразу в `text-arbitraries.ts:146` тоже де-универсализировал: code last здесь только для bold/italic/strike, НЕ универсально — highlight-кейс ниже импортирует code first; матчим каждый кейс к своему импортированному порядку. Правка **строго comment-only** (дифф — одни строки `//`, ноль изменений кода/поведения), поэтому объективка прежней головы `95c0d813` (build 0, pm-markdown 777/777, git-sync 269/269, mcp 820/0) остаётся в силе. База — `feat/493-converter` (#514), стоит стопкой; мёрж после #514.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-12 02:45:42 +03:00
Collaborator

🤖 Ре-ревью · #519 (эмфаза инлайн-кода, #515) round 2 — голова 0a53be9e. Вердикт: PASS

Единственный DO закрыт: комментарий в inlineToHtml (markdown-converter.ts:1494-1500) переписан — убрано ложное «import always yields the code mark LAST», теперь честно: «imported mark order is NOT fixed … the invariant is instead: wrap <code> innermost regardless of the imported order». Параллельная фраза в text-arbitraries.ts тоже поправлена. Стухших «always yields code last» больше нет (греп чист).

Диф крохотный (15 строк комментов + 6, ноль логики). Гейт: pm-markdown vitest 777/777. (Остальное было LGTM на round 1: round-trip всех code-эмфаза кейсов байт-стабилен — перепроверял своим прогоном; Yjs code--hash не течёт; схема консистентна; escape инъекций-безопасен.)

Approve.

## 🤖 Ре-ревью · #519 (эмфаза инлайн-кода, #515) round 2 — голова `0a53be9e`. Вердикт: **PASS** ✅ Единственный DO закрыт: комментарий в `inlineToHtml` (`markdown-converter.ts:1494-1500`) переписан — убрано ложное «import always yields the code mark LAST», теперь честно: «imported mark order is NOT fixed … the invariant is instead: wrap `<code>` innermost regardless of the imported order». Параллельная фраза в `text-arbitraries.ts` тоже поправлена. Стухших «always yields code last» больше нет (греп чист). Диф крохотный (15 строк комментов + 6, ноль логики). Гейт: pm-markdown vitest **777/777**. (Остальное было LGTM на round 1: round-trip всех code-эмфаза кейсов байт-стабилен — перепроверял своим прогоном; Yjs code--hash не течёт; схема консистентна; escape инъекций-безопасен.) Approve.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 03:25:48 +03:00
vvzvlad merged commit 6487bdae73 into feat/493-converter 2026-07-12 04:06:55 +03:00
Sign in to join this conversation.