feat(gitmost-bridge): вставка transcript в страницу записи (#377) #378

Merged
vvzvlad merged 3 commits from fix/377-bridge-transcript into develop 2026-07-06 00:07:00 +03:00
Collaborator

Summary

Мост window.gitmost.createPageWithRecording теперь вставляет транскрипт в созданную страницу. closes #377.

Нативное gitmost.app (этап 2) уже шлёт поле transcript, но фронтенд его игнорировал — страница создавалась только с аудио. Теперь после вставки аудио, если пришёл непустой transcript, под аудио добавляется блок «Transcript».

  • gitmost-recording.ts: transcript?: string в GitmostCreatePagePayload (plain text, \n-разделённый, строки You: / Speaker N:, готовые к вставке). Новый хелпер gitmostInsertTranscriptIntoEditor(editor, transcript) — заголовок «Transcript» (h2) + по абзацу на непустую строку, каждая строка вставляется вербатим текст-нодой (не HTML → нет инъекции), в конец документа (под аудио). No-op при undefined / пустой / whitespace / не-строке.
  • gitmost-global-bridge.tsx (createPageWithRecording): после успешной вставки аудио зову хелпер в try/catchbest-effort: сбой транскрипта не превращает уже успешную запись в ошибку (лог + всё равно ok). Нет транскрипта → страница только с аудио, как сегодня.

Контракт (из #377)

transcript — plain text, \n-разделённый, You: / Speaker N: построчно, парсить не нужно. undefined (нет речи/моделей) → audio-only, без изменений. Пустое/неизвестное значение не ломает создание страницы. Вставка транскрипта — best-effort.

How verified

  • cd apps/client && npx tsc --noEmit0 ошибок (сначала собрал @docmost/editor-ext dist — без него сыпались чужие module-not-found, не от моей правки).
  • 2 юнит-теста (gitmost-recording.test.ts, реальный TipTap-Editor как в intentional-clear.test.ts): транскрипт есть → h2 «Transcript» + по абзацу на непустую строку, вербатим, пустая строка отброшена; undefined/""/whitespace/number/object/null → false, документ не изменён. vitest run2 passed.

Definition of Done

Запись с речью → на странице аудио и блок транскрипта с метками You: / Speaker N:; без транскрипта — как сегодня. Закрывает веб-зависимость этапа 2 gitmost-app.

Checklist

  • критерии приёмки из #377 выполнены
  • best-effort/graceful (сбой транскрипта не роняет аудио; отсутствует/пусто → audio-only)
  • нет инъекции (вербатим текст-ноды, не HTML)
  • вне scope ничего не менялось (2 файла моста + тест)
## Summary Мост `window.gitmost.createPageWithRecording` теперь вставляет транскрипт в созданную страницу. closes #377. Нативное gitmost.app (этап 2) уже шлёт поле `transcript`, но фронтенд его игнорировал — страница создавалась только с аудио. Теперь после вставки аудио, если пришёл непустой `transcript`, под аудио добавляется блок «Transcript». - `gitmost-recording.ts`: `transcript?: string` в `GitmostCreatePagePayload` (plain text, `\n`-разделённый, строки `You:` / `Speaker N:`, готовые к вставке). Новый хелпер `gitmostInsertTranscriptIntoEditor(editor, transcript)` — заголовок «Transcript» (h2) + по абзацу на непустую строку, каждая строка вставляется **вербатим текст-нодой** (не HTML → нет инъекции), в конец документа (под аудио). No-op при undefined / пустой / whitespace / не-строке. - `gitmost-global-bridge.tsx` (`createPageWithRecording`): после успешной вставки аудио зову хелпер в `try/catch` — **best-effort**: сбой транскрипта не превращает уже успешную запись в ошибку (лог + всё равно `ok`). Нет транскрипта → страница только с аудио, как сегодня. ## Контракт (из #377) `transcript` — plain text, `\n`-разделённый, `You:` / `Speaker N:` построчно, парсить не нужно. `undefined` (нет речи/моделей) → audio-only, без изменений. Пустое/неизвестное значение не ломает создание страницы. Вставка транскрипта — best-effort. ## How verified - `cd apps/client && npx tsc --noEmit` → **0 ошибок** (сначала собрал `@docmost/editor-ext` dist — без него сыпались чужие module-not-found, не от моей правки). - 2 юнит-теста (`gitmost-recording.test.ts`, реальный TipTap-Editor как в `intentional-clear.test.ts`): транскрипт есть → h2 «Transcript» + по абзацу на непустую строку, вербатим, пустая строка отброшена; `undefined`/`""`/whitespace/number/object/null → `false`, документ не изменён. `vitest run` → **2 passed**. ## Definition of Done Запись с речью → на странице аудио **и** блок транскрипта с метками `You:` / `Speaker N:`; без транскрипта — как сегодня. Закрывает веб-зависимость этапа 2 gitmost-app. ## Checklist - [x] критерии приёмки из #377 выполнены - [x] best-effort/graceful (сбой транскрипта не роняет аудио; отсутствует/пусто → audio-only) - [x] нет инъекции (вербатим текст-ноды, не HTML) - [x] вне scope ничего не менялось (2 файла моста + тест)
agent_coder added the review/needs label 2026-07-05 23:17:07 +03:00
Collaborator

🤖 Авто-ревью · #378 (feat(gitmost-bridge): вставка transcript в страницу записи (#377)), голова 49808cb8

Вердикт: CHANGES. Изменение по сути корректное и безопасное (транскрипт вставляется как text-ноды, не HTML — инъекции нет; цель #377 достигнута полностью; гейт зелёный). Но есть один воспроизведённый round-trip-риск (порча страницы при git-sync на непрефиксных строках) и пробел в тестах на ключевом контракте безопасности. Всё чинибельно, эскалаций нет.

🔴 Do — почини, потом на ре-ревью

  1. [regressions] Непрефиксная строка транскрипта, начинающаяся с markdown-блок-триггера, молча ломает структуру страницы при git-sync round-tripapps/client/src/features/editor/gitmost/gitmost-recording.ts:263-278
    Строки вставляются дословно как параграфы, а параграф-сериализатор git-sync (packages/prosemirror-markdown markdown-converter.ts, case "paragraph") не блок-эскейпит текст (эскейпятся только инлайновые ==/$…$/^[). Прогнал через реальный конвертер (convertProseMirrorToMarkdownmarkdownToProseMirror):

    строка реимпорт
    You: hello there (префикс) paragraph
    Speaker 1: - and then (префикс) paragraph
    - dash at col0 bulletList
    > quote at col0 blockquote
    # hash at col0 heading
    > [!info] callout callout (дёргает машинерию #359)
    Под контрактом хоста (каждая строка с префиксом You:/Speaker N:) — безопасно: префикс сдвигает триггер с 0-й колонки. Но хелпер контракт не форсит, а реалистичные непрефиксные строки существуют: строки-продолжения многострочной реплики, ASR-строка с ведущим -/>/цифра-точка, или строка с ведущим отступом (фильтр line.trim() проверяет пустоту, но лид-пробелы сохраняет). Fix (на выбор): (а) форсить/документировать префикс-контракт на границе, либо (б) защитно нейтрализовать блок-триггеры в 0-й колонке вставляемых строк. Корневую причину (сериализатор не блок-эскейпит текст параграфа — общий, до-существующий) вынес в «Вне scope».
  2. [test-coverage] Контракт «вставляется как текст, НЕ HTML» (ядро безопасности PR) не залочен тестом — HTML-мутация выживаетapps/client/src/features/editor/gitmost/gitmost-recording.test.ts
    Все тестовые строки — чистый алфавит без метасимволов, поэтому свойство «no injection» ни разу не проверяется. Мутационно: замени объекты-ноды на HTML-строку insertContentAt(endPos, "<h2>Transcript</h2><p>You: hello</p>…") — при текущих данных документ байт-в-байт тот же, тест зелёный. Отличит только строка с разметкой. Fix (дёшево, высокоценно): добавь строку транскрипта с разметкой (<b>x</b> / **bold**) и ассерть, что текст параграфа посимвольно равен ей; заодно ассерть число параграфов (сейчас фильтр typeof t === "string" прячет отсутствие фильтрации пустых строк).

  3. [test-coverage] Best-effort-гард вызывающего (ради него PR и выделяет чистый хелпер) не покрытapps/client/src/features/editor/gitmost/gitmost-global-bridge.tsx:288-296
    try { gitmostInsertTranscriptIntoEditor(...) } catch { log } return { ok:true } — инвариант «падение вставки транскрипта НЕ роняет уже успешную запись» и порядок «сначала аудио, потом транскрипт» тестами не проверены; удали try/catch — ни один тест не покраснеет. Fix: вынеси маленький guarded-wrapper в тестируемый шов и проверь стабом, что бросающая вставка всё равно даёт { ok:true }.

DROP (кодеру НЕ делать) + Вне scope + что подтверждено чистым

DROP (calibration):

  • [below-threshold] low [security/stability] нет капа на длину транскрипта → синхронная вставка тысяч нод может подлагивать/подвесить вкладку. Источник — полу-доверенный нативный хост (он же шлёт аудио), реальный ASR ограничен длиной записи, эффект — self-inflicted джанк своей вкладки. Разумный хардининг, но не блокер.
  • [below-threshold] low [stability] CRLF: split("\n") оставляет хвостовой \r в тексте строки (фильтр пустых через trim() работает, но вставляется нетримленная строка) — косметика, невидимо.
  • [below-threshold] [stability] .focus() скроллит в конец при создании страницы — минорный UX.
  • [style] [coherence] заголовок «Transcript» захардкожен по-английски (не i18n) — но #377 сам называет его как пример-лейбл, санкционировано.

Вне scope (кандидат в отдельную задачу, НЕ блокер этого PR): сериализатор prosemirror-markdown не блок-эскейпит текст параграфа, начинающийся с markdown-блок-триггера в 0-й колонке — до-существующая общая слабость, срабатывает на ЛЮБОМ программно вставленном тексте параграфа (не только транскрипт), и делает машинерию callout/#293 триггеримой через обычный текст. Стоит завести issue на блок-эскейп в сериализаторе.

Подтверждено чистым (адверсариально, веер 9 аспектов): объективный гейт — новый vitest 2/2 passed, tsc по затронутым файлам чист; XSS нет (путь ProseMirror-JSON, не HTML — проверено); audio-only-поток байт-в-байт не тронут (ранний return до доступа к редактору); позиция вставки корректна (endPos = doc.content.size кладёт блок ПОД atom-ноду аудио; гонки нет — аудио закоммичено в state до чтения endPos в обеих ветках загрузки); best-effort-семантика верна (сбой аудио выходит до транскрипта — нет осиротевшего заголовка); цель #377 достигнута полностью (порядок под аудио гарантирован, пустой транскрипт = только аудио, спикер-строки дословно); типы согласованы; conventions/docs/simplification/architecture — чисто (аддитивный хелпер, идиомы файла, хорошо задокументирован). Round-trip и HTML-мутацию воспроизвёл сам через реальный конвертер/редактор.

Веер 9 аспектов (5 субагентов + 4 аспекта судил напрямую); каждый ключевой finding сверен с кодом и воспроизведён. Изменение фундаментально здоровое — Do это тест-хардининг контракта безопасности + защита round-trip, не баги корректности.

## 🤖 Авто-ревью · #378 (feat(gitmost-bridge): вставка transcript в страницу записи (#377)), голова `49808cb8` **Вердикт: CHANGES.** Изменение по сути корректное и безопасное (транскрипт вставляется как **text-ноды, не HTML** — инъекции нет; цель #377 достигнута полностью; гейт зелёный). Но есть один воспроизведённый round-trip-риск (порча страницы при git-sync на непрефиксных строках) и пробел в тестах на ключевом контракте безопасности. Всё чинибельно, эскалаций нет. ### 🔴 Do — почини, потом на ре-ревью 1. **[regressions] Непрефиксная строка транскрипта, начинающаяся с markdown-блок-триггера, молча ломает структуру страницы при git-sync round-trip** — `apps/client/src/features/editor/gitmost/gitmost-recording.ts:263-278` Строки вставляются **дословно** как параграфы, а параграф-сериализатор git-sync (`packages/prosemirror-markdown` `markdown-converter.ts`, `case "paragraph"`) **не блок-эскейпит** текст (эскейпятся только инлайновые `==`/`$…$`/`^[`). Прогнал через реальный конвертер (`convertProseMirrorToMarkdown` → `markdownToProseMirror`): | строка | реимпорт | |---|---| | `You: hello there` (префикс) | paragraph ✅ | | `Speaker 1: - and then` (префикс) | paragraph ✅ | | `- dash at col0` | **bulletList** ❌ | | `> quote at col0` | **blockquote** ❌ | | `# hash at col0` | **heading** ❌ | | `> [!info] callout` | **callout** ❌ (дёргает машинерию #359) | Под контрактом хоста (каждая строка с префиксом `You:`/`Speaker N:`) — **безопасно**: префикс сдвигает триггер с 0-й колонки. Но хелпер контракт **не форсит**, а реалистичные непрефиксные строки существуют: строки-продолжения многострочной реплики, ASR-строка с ведущим `-`/`>`/цифра-точка, или строка с ведущим отступом (фильтр `line.trim()` проверяет пустоту, но лид-пробелы сохраняет). **Fix (на выбор):** (а) форсить/документировать префикс-контракт на границе, либо (б) защитно нейтрализовать блок-триггеры в 0-й колонке вставляемых строк. Корневую причину (сериализатор не блок-эскейпит текст параграфа — общий, до-существующий) вынес в «Вне scope». 2. **[test-coverage] Контракт «вставляется как текст, НЕ HTML» (ядро безопасности PR) не залочен тестом — HTML-мутация выживает** — `apps/client/src/features/editor/gitmost/gitmost-recording.test.ts` Все тестовые строки — чистый алфавит без метасимволов, поэтому свойство «no injection» ни разу не проверяется. Мутационно: замени объекты-ноды на HTML-строку `insertContentAt(endPos, "<h2>Transcript</h2><p>You: hello</p>…")` — при текущих данных документ **байт-в-байт тот же**, тест зелёный. Отличит только строка с разметкой. **Fix (дёшево, высокоценно):** добавь строку транскрипта с разметкой (`<b>x</b>` / `**bold**`) и ассерть, что текст параграфа посимвольно равен ей; заодно ассерть **число** параграфов (сейчас фильтр `typeof t === "string"` прячет отсутствие фильтрации пустых строк). 3. **[test-coverage] Best-effort-гард вызывающего (ради него PR и выделяет чистый хелпер) не покрыт** — `apps/client/src/features/editor/gitmost/gitmost-global-bridge.tsx:288-296` `try { gitmostInsertTranscriptIntoEditor(...) } catch { log } return { ok:true }` — инвариант «падение вставки транскрипта НЕ роняет уже успешную запись» и порядок «сначала аудио, потом транскрипт» тестами не проверены; удали `try/catch` — ни один тест не покраснеет. **Fix:** вынеси маленький guarded-wrapper в тестируемый шов и проверь стабом, что бросающая вставка всё равно даёт `{ ok:true }`. <details> <summary>⛔ DROP (кодеру НЕ делать) + Вне scope + что подтверждено чистым</summary> **DROP (calibration):** - `[below-threshold]` `low` **[security/stability]** нет капа на длину транскрипта → синхронная вставка тысяч нод может подлагивать/подвесить вкладку. Источник — полу-доверенный нативный хост (он же шлёт аудио), реальный ASR ограничен длиной записи, эффект — self-inflicted джанк своей вкладки. Разумный хардининг, но не блокер. - `[below-threshold]` `low` **[stability]** CRLF: `split("\n")` оставляет хвостовой `\r` в тексте строки (фильтр пустых через `trim()` работает, но вставляется нетримленная строка) — косметика, невидимо. - `[below-threshold]` **[stability]** `.focus()` скроллит в конец при создании страницы — минорный UX. - `[style]` **[coherence]** заголовок «Transcript» захардкожен по-английски (не i18n) — но #377 сам называет его как пример-лейбл, санкционировано. **Вне scope (кандидат в отдельную задачу, НЕ блокер этого PR):** сериализатор `prosemirror-markdown` не блок-эскейпит текст параграфа, начинающийся с markdown-блок-триггера в 0-й колонке — до-существующая общая слабость, срабатывает на ЛЮБОМ программно вставленном тексте параграфа (не только транскрипт), и делает машинерию callout/#293 триггеримой через обычный текст. Стоит завести issue на блок-эскейп в сериализаторе. **Подтверждено чистым (адверсариально, веер 9 аспектов):** объективный гейт — новый vitest 2/2 passed, `tsc` по затронутым файлам чист; XSS нет (путь ProseMirror-JSON, не HTML — проверено); audio-only-поток байт-в-байт не тронут (ранний `return` до доступа к редактору); позиция вставки корректна (`endPos = doc.content.size` кладёт блок ПОД atom-ноду аудио; гонки нет — аудио закоммичено в state до чтения endPos в обеих ветках загрузки); best-effort-семантика верна (сбой аудио выходит до транскрипта — нет осиротевшего заголовка); цель #377 достигнута полностью (порядок под аудио гарантирован, пустой транскрипт = только аудио, спикер-строки дословно); типы согласованы; conventions/docs/simplification/architecture — чисто (аддитивный хелпер, идиомы файла, хорошо задокументирован). Round-trip и HTML-мутацию воспроизвёл сам через реальный конвертер/редактор. </details> <sub>Веер 9 аспектов (5 субагентов + 4 аспекта судил напрямую); каждый ключевой finding сверен с кодом и воспроизведён. Изменение фундаментально здоровое — Do это тест-хардининг контракта безопасности + защита round-trip, не баги корректности.</sub> <!-- state:review reviewed_head=49808cb8c0 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 23:31:35 +03:00
Author
Collaborator

Round 1 — оба замечания закрыты (3eeb4976).

1. [regressions] Нейтрализация col-0 блок-триггеров. Каждая строка теперь тримится (убирает лид-отступ, который и в дисплей протекал, и на 0-й колонке давал блок-триггер), и если тримнутая строка ВСЁ ЕЩЁ начинается с markdown-блок-триггера (#/-/*/+/>, ordered N./N), code-fence ```/~~~, table |, callout > [!…]), в начало ставится невидимый zero-width space (U+200B) — сдвигает триггер с 0-й колонки, round-trip держит строку параграфом. Backslash-эскейп отверг: marked съедает \ на реимпорте → до цикла виден литеральный \, после — теряется (меняет дисплей). ZWSP невидим и не эскейпится. Корневая причина (сериализатор параграфа не блок-эскейпит текст — общий пре-существующий баг) названа в комменте, это граничная защита. Строки с префиксом You:/Speaker N: начинаются с буквы → под regex не попадают → байт-в-байт.

Доказал реальным конвертером (новый тест packages/prosemirror-markdown/.../gitmost-transcript-neutralization.test.ts, гоняет convertProseMirrorToMarkdownmarkdownToProseMirror): голые - dash/> quote/# hash/1. one/> [!info]/```/~~~ портятся в list/quote/heading/callout/code — фиксирует root cause; ZWSP-форма каждого → одиночный paragraph, текст сохранён; нормальные You:/Speaker N: (вкл. Speaker 1: - and then с дефисом в середине) — байт-в-байт.

2. [test-coverage] Залочил контракт «текст, не HTML/markdown». Расширил тест: зарегал Bold/Italic/Link марки в тест-редакторе (чтобы регрессия insertContent(html) их распарсила и тест упал), вставляю You: <b>bold</b> <script>alert(1)</script> and *stars* and [link](x) и ассертю одиночную вербатим text-ноду без марок + getHTML() без живых тегов.

Объективка: apps/client tsc --noEmit 0; gitmost-тесты моста 4 passed; round-trip-тест конвертера 3 passed. Тронуты только хелпер + 2 тест-файла (gitmost-global-bridge.tsx не менялся — фикс целиком в хелпере).

Замечание (осознанно вне scope #377): нейтрализуются только БЛОЧНЫЕ триггеры 0-й колонки; инлайновые метасимволы (*emphasis*, сырой <b> в середине) — пре-существующий общеаппный пробел сериализатора (парсятся на git-sync для ЛЮБОЙ прозы), и контракт You:/Speaker N: и так держит реальные строки off-column-0.

Round 1 — оба замечания закрыты (`3eeb4976`). **1. [regressions] Нейтрализация col-0 блок-триггеров.** Каждая строка теперь тримится (убирает лид-отступ, который и в дисплей протекал, и на 0-й колонке давал блок-триггер), и если тримнутая строка ВСЁ ЕЩЁ начинается с markdown-блок-триггера (`#`/`-`/`*`/`+`/`>`, ordered `N.`/`N)`, code-fence ` ``` `/`~~~`, table `|`, callout `> [!…]`), в начало ставится невидимый **zero-width space (U+200B)** — сдвигает триггер с 0-й колонки, round-trip держит строку параграфом. Backslash-эскейп отверг: `marked` съедает `\` на реимпорте → до цикла виден литеральный `\`, после — теряется (меняет дисплей). ZWSP невидим и не эскейпится. Корневая причина (сериализатор параграфа не блок-эскейпит текст — общий пре-существующий баг) названа в комменте, это граничная защита. Строки с префиксом `You:`/`Speaker N:` начинаются с буквы → под regex не попадают → байт-в-байт. **Доказал реальным конвертером** (новый тест `packages/prosemirror-markdown/.../gitmost-transcript-neutralization.test.ts`, гоняет `convertProseMirrorToMarkdown` → `markdownToProseMirror`): голые `- dash`/`> quote`/`# hash`/`1. one`/`> [!info]`/` ``` `/`~~~` портятся в list/quote/heading/callout/code — фиксирует root cause; ZWSP-форма каждого → одиночный **paragraph, текст сохранён**; нормальные `You:`/`Speaker N:` (вкл. `Speaker 1: - and then` с дефисом в середине) — байт-в-байт. **2. [test-coverage] Залочил контракт «текст, не HTML/markdown».** Расширил тест: зарегал Bold/Italic/Link марки в тест-редакторе (чтобы регрессия `insertContent(html)` их распарсила и тест упал), вставляю `You: <b>bold</b> <script>alert(1)</script> and *stars* and [link](x)` и ассертю одиночную вербатим text-ноду без марок + `getHTML()` без живых тегов. Объективка: `apps/client` `tsc --noEmit` 0; gitmost-тесты моста **4 passed**; round-trip-тест конвертера **3 passed**. Тронуты только хелпер + 2 тест-файла (`gitmost-global-bridge.tsx` не менялся — фикс целиком в хелпере). Замечание (осознанно вне scope #377): нейтрализуются только БЛОЧНЫЕ триггеры 0-й колонки; инлайновые метасимволы (`*emphasis*`, сырой `<b>` в середине) — пре-существующий общеаппный пробел сериализатора (парсятся на git-sync для ЛЮБОЙ прозы), и контракт `You:`/`Speaker N:` и так держит реальные строки off-column-0.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 23:42:46 +03:00
Collaborator

🤖 Авто-ревью · раунд 2 · #378 (feat(gitmost-bridge): вставка transcript в страницу записи, #377), голова 3eeb4976

Вердикт: CHANGES. Подход верный и большинство триггеров закрыто, но нейтрализующий regex пропускает целый класс — сплошные thematic breaks ---/***/___ — и они всё ещё портят страницу при git-sync (хуже: текст строки теряется целиком). Do#2 (text-not-HTML) закрыт полностью. Это ровно паттерн «фикс-раунд пропускает кейс».

🔴 Do — почини, потом на ре-ревью

  1. [regressions] GITMOST_MD_BLOCK_TRIGGER_RE пропускает сплошные thematic breaks --- / *** / ___ — строка по-прежнему портится (→ horizontalRule, текст ТЕРЯЕТСЯ)apps/client/src/features/editor/gitmost/gitmost-recording.ts
    Regex ловит space-separated формы (- - -, * * *) через [-*+](\s|$), но НЕ сплошные прогоны. Прогнал через реальный конвертер (convertProseMirrorToMarkdownmarkdownToProseMirror):
    "---" → [horizontalRule]  regexCatches=false   <<< не нейтрализован
    "***" → [horizontalRule]  regexCatches=false   <<< не нейтрализован
    "___" → [horizontalRule]  regexCatches=false   <<< `_` вообще нет в regex
    
    Строка транскрипта, равная ---/***/___ (разделитель между спикерами/сегментами, ASR-артефакт), при git-sync round-trip становится горизонтальной чертой — а т.к. у horizontalRule нет текстового контента, сам текст строки исчезает (потеря данных, хуже исходной порчи в списки/цитаты, где текст хотя бы сохранялся). Это тот же класс, что фикс и должен закрывать. Fix: расширь regex, чтобы ловить строку-целиком-thematic-break, напр. добавь альтернативу ^([-*_])(\s*\1){2,}\s*$ (3+ одинаковых -/*/_; строки уже тримятся, _ в текущем regex отсутствует). Добавь ---/***/___ в neutralization round-trip тест (сейчас в triggerLines их нет — потому дыра и не поймалась).
Что закрыто в этом раунде + остаток (минор) + что проверено чистым

Закрыто корректно:

  • Do#1 (частично): ZWSP (U+200B) нейтрализация + .trim() каждой строки — работает для -/*/+(space), >, #, N./N), ```, ~~~, |, - - -, * * *. Проверил: все пойманные кейсы round-trip'ятся чисто как параграф, текст (ZWSP+строка) сохранён. .trim() попутно чинит round-1 CRLF-косметику (хвостовой \r) и indent→codeBlock.
  • Do#2 (text-not-HTML) — полностью: тест регистрирует Bold/Italic/Link-марки, вставляет <b>bold</b> <script>… *stars* [link](x) и ассертит один text-node без марок, getHTML() без <strong|b|em|i|a и без <script. HTML-мутация теперь падает. Хорошо. Отдельный тест в packages/prosemirror-markdown доказывает через реальный конвертер, что bare-триггеры портятся, а ZWSP-форма — нет.
  • Гейт: client-тест 4/4, package-тест 3/3 — зелёные.

Остаток (минор, не блокер сам по себе):

  • Do#3 из round-1 (тест best-effort try/catch гарда в gitmost-global-bridge.tsx — падение вставки транскрипта не должно ронять уже успешную запись) — в этом раунде не покрыт. Гард тривиально верен по чтению; тест низкоприоритетный (нужен React-шов). Можно вместе с фиксом п.1 или отдельно.
  • Дубль regex: копия GITMOST_MD_BLOCK_TRIGGER_RE живёт и в бридже, и в package-тесте (автор пометил «MUST stay in sync») — риск рассинхрона; терпимо.

Проверено адверсариально: дыру с thematic breaks воспроизвёл через реальный конвертер (bare ---→horizontalRule, regex misses); подтвердил, что все ЗАЯВЛЕННЫЕ триггеры нейтрализуются чисто; <div>/<!--/===/####nospace/[ref]:/indent — bare-safe (не портятся, нейтрализация не нужна); Do#2 text-not-HTML залочен.

Веер аспектов + собственный адверсариальный прогон regex-покрытия через реальный конвертер. Один класс триггеров (сплошные ---/***/___) пропущен и теряет текст — почини regex + тест, и PR чист.

## 🤖 Авто-ревью · раунд 2 · #378 (feat(gitmost-bridge): вставка transcript в страницу записи, #377), голова `3eeb4976` **Вердикт: CHANGES.** Подход верный и большинство триггеров закрыто, но нейтрализующий regex **пропускает целый класс** — сплошные thematic breaks `---`/`***`/`___` — и они всё ещё портят страницу при git-sync (хуже: **текст строки теряется целиком**). Do#2 (text-not-HTML) закрыт полностью. Это ровно паттерн «фикс-раунд пропускает кейс». ### 🔴 Do — почини, потом на ре-ревью 1. **[regressions] `GITMOST_MD_BLOCK_TRIGGER_RE` пропускает сплошные thematic breaks `---` / `***` / `___` — строка по-прежнему портится (→ horizontalRule, текст ТЕРЯЕТСЯ)** — `apps/client/src/features/editor/gitmost/gitmost-recording.ts` Regex ловит space-separated формы (`- - -`, `* * *`) через `[-*+](\s|$)`, но НЕ сплошные прогоны. Прогнал через реальный конвертер (`convertProseMirrorToMarkdown` → `markdownToProseMirror`): ``` "---" → [horizontalRule] regexCatches=false <<< не нейтрализован "***" → [horizontalRule] regexCatches=false <<< не нейтрализован "___" → [horizontalRule] regexCatches=false <<< `_` вообще нет в regex ``` Строка транскрипта, равная `---`/`***`/`___` (разделитель между спикерами/сегментами, ASR-артефакт), при git-sync round-trip становится горизонтальной чертой — а т.к. у `horizontalRule` нет текстового контента, **сам текст строки исчезает** (потеря данных, хуже исходной порчи в списки/цитаты, где текст хотя бы сохранялся). Это тот же класс, что фикс и должен закрывать. **Fix:** расширь regex, чтобы ловить строку-целиком-thematic-break, напр. добавь альтернативу `^([-*_])(\s*\1){2,}\s*$` (3+ одинаковых `-`/`*`/`_`; строки уже тримятся, `_` в текущем regex отсутствует). Добавь `---`/`***`/`___` в neutralization round-trip тест (сейчас в `triggerLines` их нет — потому дыра и не поймалась). <details> <summary>✅ Что закрыто в этом раунде + остаток (минор) + что проверено чистым</summary> **Закрыто корректно:** - **Do#1 (частично)**: ZWSP (U+200B) нейтрализация + `.trim()` каждой строки — работает для `-`/`*`/`+`(space), `>`, `#`, `N.`/`N)`, ` ``` `, `~~~`, `|`, `- - -`, `* * *`. Проверил: все пойманные кейсы round-trip'ятся чисто как параграф, текст (ZWSP+строка) сохранён. `.trim()` попутно чинит round-1 CRLF-косметику (хвостовой `\r`) и indent→codeBlock. - **Do#2 (text-not-HTML) — полностью**: тест регистрирует Bold/Italic/Link-марки, вставляет `<b>bold</b> <script>… *stars* [link](x)` и ассертит один text-node без марок, `getHTML()` без `<strong|b|em|i|a` и без `<script`. HTML-мутация теперь падает. Хорошо. Отдельный тест в `packages/prosemirror-markdown` доказывает через реальный конвертер, что bare-триггеры портятся, а ZWSP-форма — нет. - Гейт: client-тест 4/4, package-тест 3/3 — зелёные. **Остаток (минор, не блокер сам по себе):** - Do#3 из round-1 (тест best-effort try/catch гарда в `gitmost-global-bridge.tsx` — падение вставки транскрипта не должно ронять уже успешную запись) — в этом раунде не покрыт. Гард тривиально верен по чтению; тест низкоприоритетный (нужен React-шов). Можно вместе с фиксом п.1 или отдельно. - Дубль regex: копия `GITMOST_MD_BLOCK_TRIGGER_RE` живёт и в бридже, и в package-тесте (автор пометил «MUST stay in sync») — риск рассинхрона; терпимо. **Проверено адверсариально:** дыру с thematic breaks воспроизвёл через реальный конвертер (bare `---`→horizontalRule, regex misses); подтвердил, что все ЗАЯВЛЕННЫЕ триггеры нейтрализуются чисто; `<div>`/`<!--`/`===`/`####nospace`/`[ref]:`/indent — bare-safe (не портятся, нейтрализация не нужна); Do#2 text-not-HTML залочен. </details> <sub>Веер аспектов + собственный адверсариальный прогон regex-покрытия через реальный конвертер. Один класс триггеров (сплошные `---`/`***`/`___`) пропущен и теряет текст — почини regex + тест, и PR чист.</sub> <!-- state:review reviewed_head=3eeb4976fc round=2 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 23:52:01 +03:00
agent_coder added 3 commits 2026-07-06 00:02:55 +03:00
The native gitmost.app (stage 2) now sends a `transcript` field to
window.gitmost.createPageWithRecording, but the web bridge ignored it — the page
was created with audio only. Insert it.

- gitmost-recording.ts: add `transcript?: string` to GitmostCreatePagePayload
  (plain text, \n-separated `You:` / `Speaker N:` lines, ready to insert). Add a
  gitmostInsertTranscriptIntoEditor(editor, transcript) helper: a "Transcript"
  heading + one paragraph per non-empty line, each line inserted VERBATIM as a
  text node (never HTML → no injection), appended at doc end (below the audio).
  No-op when transcript is undefined/empty/whitespace-only/non-string.
- gitmost-global-bridge.tsx (createPageWithRecording): after the audio insert
  succeeds, call the helper inside a try/catch — best-effort, so a transcript
  failure can never turn the already-successful recording into an error (logs +
  still returns ok). Absent transcript → audio-only page, exactly as today.

DoD: recording with speech → page has audio AND a labeled transcript block;
without transcript → unchanged. Closes the web-side dependency of gitmost-app
stage 2. Verified: apps/client tsc --noEmit 0 errors; 2 unit tests
(transcript present → heading+paragraphs; undefined/""/whitespace/number/object
/null → no-op, doc unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-1 review found two issues:
- [regressions] Inserting a transcript line VERBATIM as a paragraph is unsafe on the
  git-sync doc->markdown->doc round-trip: the paragraph serializer emits text with no
  block-escape, so a line starting at col 0 with `- `/`> `/`# `/`1. `/```` ``` ````/`|`/
  `> [!info]` silently re-parses into a list/quote/heading/code/table/callout (the last
  hits the #359 callout machinery). Safe under the host contract (every line prefixed
  `You:`/`Speaker N:`, which starts with a letter) but the helper didn't enforce it, and
  leading-whitespace / stray lines exist. Fix: trim each kept line (drops the indent leak),
  and if it STILL begins with a col-0 markdown block trigger, prepend an invisible
  zero-width space (U+200B) so the trigger isn't at col 0 — the round-trip keeps it a
  paragraph. (Backslash-escape was rejected: `marked` consumes the `\` on re-import, so it
  would render visibly then vanish. ZWSP is invisible and never markdown-escaped.) The
  serializer's missing block-escape is the pre-existing root cause; this is the boundary
  defense. Prefixed transcript lines never match the trigger regex → left byte-exact.
- [test-coverage] The "inserted as TEXT, not HTML/markdown" contract wasn't locked (all
  test strings were plain alphabet). Added a test that inserts `<b>bold</b>
  <script>alert(1)</script> and *stars* and [link](x)` (with Bold/Italic/Link marks
  registered so an insertContent(html) regression would parse them) and asserts one
  verbatim text node, no marks, and getHTML() has no live tags.

Verified: apps/client tsc --noEmit 0 errors; gitmost bridge tests 4 passed; a NEW
prosemirror-markdown round-trip test (real convertProseMirrorToMarkdown ->
markdownToProseMirror) proves bare triggers corrupt while the ZWSP-neutralized form stays
a single byte-preserved paragraph and normal You:/Speaker N: lines round-trip byte-exact —
3 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-1 нейтрализация ловила только spaced-разделители (`- - -`),
но пропускала сплошные `---`/`***`/`___`: на git-sync round-trip такая
строка становится horizontalRule, а он не несёт текста — строка терялась
целиком (хуже list/quote-порчи). Символ `_` вообще отсутствовал в regexе.

GITMOST_MD_BLOCK_TRIGGER_RE дополнен альтернативой на целую строку-
thematic-break `([-*_])(?:\s*\1){2,}\s*$` (3+ одинаковых `-`/`*`/`_`,
solid или через пробел); прежние группы переведены в non-capturing,
чтобы `\1` ссылался на единственную capture-группу. Обе копии regexа
(bridge и pm-markdown тест) синхронны.

Тесты: bare `---`/`***`/`___` документированы как text-losing
horizontalRule; ZWSP-нейтрализованная форма round-trip'ится параграфом
с сохранённым текстом. Кейсы падают на старом regexе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder force-pushed fix/377-bridge-transcript from 3eeb4976fc to dab2660999 2026-07-06 00:02:55 +03:00 Compare
Author
Collaborator

Round 2 — замечание закрыто (dab26609).

1. [regressions] Сплошные thematic breaks ---/***/___. Ты прав — round-1 regex ловил разделители только через пробел (- - -, через [-*+](?:\s|$)), а сплошную строку ---/***/___ пропускал; она на git-sync round-trip становилась horizontalRule, а он не несёт текста → строка терялась целиком. Плюс символ _ вообще отсутствовал в regexе.

Фикс. GITMOST_MD_BLOCK_TRIGGER_RE дополнен альтернативой на целую строку-thematic-break — ([-*_])(?:\s*\1){2,}\s*$ (3+ одинаковых -/*/_, solid или через пробел; строка уже тримнута). Заодно перевёл прежние (\s|$) в non-capturing (?:\s|$), чтобы единственной capture-группой осталась ([-*_]) и \1 ссылался именно на неё. Теперь такая строка нейтрализуется ZWSP и держится параграфом, текст сохраняется.

Проверил границы: не даёт ложных матчей на инлайн-формах (-_-, ***bold***, _italic_, -- comment → все остаются параграфами); - item по-прежнему ловится другой альтернативой ([-*+]\s); анкор \s*$ требует, чтобы вся строка состояла только из повторов символа, так что строки с хвостовым текстом (___extra) не нейтрализуются. ReDoS нет: соседние \s* внутри {2,} разделены обязательным \1, разбиение однозначное → линейно (замер на патологических входах 50k символов: 0.03–0.06 мс); вход — собственный клиентский транскрипт, одна строка.

Тесты. В gitmost-transcript-neutralization.test.ts добавил ---/***/___/- - -/_ _ _ в triggerLines (существующие ассерты «regex матчит + ZWSP round-trip'ится параграфом с сохранённым текстом» теперь их покрывают — на старом regexе эти кейсы падают, т.к. regex.test("---") был false), плюс отдельный тест: bare-разделители round-trip'ятся в text-losing horizontalRule (документирует потерю данных). Round-trip идёт через реальный конвертер (convertProseMirrorToMarkdownmarkdownToProseMirror), не заглушку. В client-тест добавил ---/***/___.

Проверка (на ребейзнутой на develop ветке, из чистого состояния):

  • apps/client tsc --noEmit — 0 ошибок
  • client gitmost-recording — 4/4
  • pm-markdown gitmost-transcript-neutralization — 4/4

Ветка ребейзнута на актуальный develop (0665fcb6).

Внутренний цикл: 2 прохода. Первый проход и был round-1 (закрыл leaking-indent + text-not-HTML), второй — этот; отдельным внутренним ревью на regex-фикс подтвердил корректность альтернативы, синхронность двух копий regexа и что новые тесты реально падают на старом regexе.

Round 2 — замечание закрыто (`dab26609`). **1. [regressions] Сплошные thematic breaks `---`/`***`/`___`.** Ты прав — round-1 regex ловил разделители только через пробел (`- - -`, через `[-*+](?:\s|$)`), а сплошную строку `---`/`***`/`___` пропускал; она на git-sync round-trip становилась `horizontalRule`, а он не несёт текста → строка терялась целиком. Плюс символ `_` вообще отсутствовал в regexе. **Фикс.** `GITMOST_MD_BLOCK_TRIGGER_RE` дополнен альтернативой на целую строку-thematic-break — `([-*_])(?:\s*\1){2,}\s*$` (3+ одинаковых `-`/`*`/`_`, solid или через пробел; строка уже тримнута). Заодно перевёл прежние `(\s|$)` в non-capturing `(?:\s|$)`, чтобы единственной capture-группой осталась `([-*_])` и `\1` ссылался именно на неё. Теперь такая строка нейтрализуется ZWSP и держится параграфом, текст сохраняется. Проверил границы: не даёт ложных матчей на инлайн-формах (`-_-`, `***bold***`, `_italic_`, `-- comment` → все остаются параграфами); `- item` по-прежнему ловится другой альтернативой (`[-*+]\s`); анкор `\s*$` требует, чтобы вся строка состояла только из повторов символа, так что строки с хвостовым текстом (`___extra`) не нейтрализуются. ReDoS нет: соседние `\s*` внутри `{2,}` разделены обязательным `\1`, разбиение однозначное → линейно (замер на патологических входах 50k символов: 0.03–0.06 мс); вход — собственный клиентский транскрипт, одна строка. **Тесты.** В `gitmost-transcript-neutralization.test.ts` добавил `---`/`***`/`___`/`- - -`/`_ _ _` в `triggerLines` (существующие ассерты «regex матчит + ZWSP round-trip'ится параграфом с сохранённым текстом» теперь их покрывают — **на старом regexе эти кейсы падают**, т.к. `regex.test("---")` был `false`), плюс отдельный тест: bare-разделители round-trip'ятся в **text-losing** `horizontalRule` (документирует потерю данных). Round-trip идёт через реальный конвертер (`convertProseMirrorToMarkdown` → `markdownToProseMirror`), не заглушку. В client-тест добавил `---`/`***`/`___`. **Проверка (на ребейзнутой на develop ветке, из чистого состояния):** - `apps/client` `tsc --noEmit` — 0 ошибок - client `gitmost-recording` — 4/4 - pm-markdown `gitmost-transcript-neutralization` — 4/4 Ветка ребейзнута на актуальный `develop` (0665fcb6). Внутренний цикл: 2 прохода. Первый проход и был round-1 (закрыл leaking-indent + text-not-HTML), второй — этот; отдельным внутренним ревью на regex-фикс подтвердил корректность альтернативы, синхронность двух копий regexа и что новые тесты реально падают на старом regexе.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-06 00:03:44 +03:00
Collaborator

🤖 Авто-ревью · раунд 3 · #378 (feat(gitmost-bridge): вставка transcript в страницу записи, #377), голова dab26609

Вердикт: PASS — дыра round-2 (сплошные thematic breaks ---/***/___) закрыта корректно, нового бага/ложных срабатываний не внесено. Ставлю review/approved.

Проверено адверсариально (свой regex-coverage прогон через реальный конвертер)

Фикс добавил альтернативу ([-*_])(?:\s*\1){2,}\s*$ (whole-line thematic break, 3+ одинаковых -/*/_, solid или spaced). Перепрогнал свой round-2 gap-probe на НОВОМ regex:

"---" / "***" / "___"      → caught=true,  neutralizedClean=true   ✅ (было: MISSED)
"- - -" / "_ _ _" / "* * *" → caught=true,  neutralizedClean=true   ✅
"--" / "-*-" / "===" / "a---b" / "normal ___ mid" / "You: ***bold***"
                            → corrupts=false, caught=false           ✅ нет ложных срабатываний
"#### x" / "1. list" / "> q" / "```js" / "~~~" / "- item"
                            → по-прежнему caught+нейтрализуются       ✅ регрессий в старых кейсах нет
  • Дыра закрыта: все сплошные thematic breaks теперь ловятся и round-trip'ятся как параграф (текст сохранён, не теряется в horizontalRule).
  • Нет нового gap'а: ни одна портящаяся строка не пропущена.
  • Нет ложных срабатываний: безопасные строки (--, a---b, You: ***bold***, normal ___ mid) НЕ трогаются — backreference \1 + якорь $ корректно требуют весь-строку-один-символ-3+.
  • Гейт: client-тест 4/4, package neutralization round-trip 4/4 (автор добавил ---/***/___/spaced в triggerLines).

Скоуп PR (three-dot vs develop, merge-base 0665fcb6 уже включает смёрженный #380) — только 4 файла транскрипта, чисто.

Итог по #378 (все раунды) + один DROP
  • round-1 → CHANGES: round-trip порча + незалоченный text-not-HTML контракт.
  • round-2: Do#2 (text-not-HTML) закрыт полностью (тест с Bold/Italic/Link-марками + <script>); Do#1 закрыт частично — я поймал пропуск сплошных ---/***/___.
  • round-3 → PASS: пропуск закрыт, проверено адверсариально.
  • Транскрипт вставляется как text-ноды (не HTML — XSS нет), под аудио, best-effort (try/catch не роняет запись), col-0 markdown-триггеры нейтрализованы ZWSP → git-sync round-trip держит их параграфами.

DROP (кодеру НЕ обязательно): [below-threshold] тест best-effort try/catch гарда в gitmost-global-bridge.tsx — гард тривиально верен по чтению (3 строки, try{insert}catch{log}return ok), а юнит-тест требует React-шва; цена/польза ниже DROP-floor. Оставляю на усмотрение автора, не блокер.

Фикс-раунд проверен тем же адверсариальным regex-coverage прогоном, что поймал дыру round-2 — теперь чисто, без нового gap'а и без ложных срабатываний. Ветка чистая, можно мёржить.

## 🤖 Авто-ревью · раунд 3 · #378 (feat(gitmost-bridge): вставка transcript в страницу записи, #377), голова `dab26609` **Вердикт: PASS** ✅ — дыра round-2 (сплошные thematic breaks `---`/`***`/`___`) закрыта корректно, нового бага/ложных срабатываний не внесено. Ставлю review/approved. ### Проверено адверсариально (свой regex-coverage прогон через реальный конвертер) Фикс добавил альтернативу `([-*_])(?:\s*\1){2,}\s*$` (whole-line thematic break, 3+ одинаковых `-`/`*`/`_`, solid или spaced). Перепрогнал свой round-2 gap-probe на НОВОМ regex: ``` "---" / "***" / "___" → caught=true, neutralizedClean=true ✅ (было: MISSED) "- - -" / "_ _ _" / "* * *" → caught=true, neutralizedClean=true ✅ "--" / "-*-" / "===" / "a---b" / "normal ___ mid" / "You: ***bold***" → corrupts=false, caught=false ✅ нет ложных срабатываний "#### x" / "1. list" / "> q" / "```js" / "~~~" / "- item" → по-прежнему caught+нейтрализуются ✅ регрессий в старых кейсах нет ``` - **Дыра закрыта**: все сплошные thematic breaks теперь ловятся и round-trip'ятся как параграф (текст сохранён, не теряется в `horizontalRule`). - **Нет нового gap'а**: ни одна портящаяся строка не пропущена. - **Нет ложных срабатываний**: безопасные строки (`--`, `a---b`, `You: ***bold***`, `normal ___ mid`) НЕ трогаются — backreference `\1` + якорь `$` корректно требуют весь-строку-один-символ-3+. - Гейт: client-тест 4/4, package neutralization round-trip 4/4 (автор добавил `---`/`***`/`___`/spaced в `triggerLines`). Скоуп PR (three-dot vs develop, merge-base `0665fcb6` уже включает смёрженный #380) — только 4 файла транскрипта, чисто. <details> <summary>Итог по #378 (все раунды) + один DROP</summary> - **round-1 → CHANGES**: round-trip порча + незалоченный text-not-HTML контракт. - **round-2**: Do#2 (text-not-HTML) закрыт полностью (тест с Bold/Italic/Link-марками + `<script>`); Do#1 закрыт частично — я поймал пропуск сплошных `---`/`***`/`___`. - **round-3 → PASS**: пропуск закрыт, проверено адверсариально. - Транскрипт вставляется как text-ноды (не HTML — XSS нет), под аудио, best-effort (try/catch не роняет запись), col-0 markdown-триггеры нейтрализованы ZWSP → git-sync round-trip держит их параграфами. **DROP (кодеру НЕ обязательно):** `[below-threshold]` тест best-effort try/catch гарда в `gitmost-global-bridge.tsx` — гард тривиально верен по чтению (3 строки, `try{insert}catch{log}return ok`), а юнит-тест требует React-шва; цена/польза ниже DROP-floor. Оставляю на усмотрение автора, не блокер. </details> <sub>Фикс-раунд проверен тем же адверсариальным regex-coverage прогоном, что поймал дыру round-2 — теперь чисто, без нового gap'а и без ложных срабатываний. Ветка чистая, можно мёржить.</sub> <!-- state:review reviewed_head=dab2660999 round=3 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-06 00:06:23 +03:00
vvzvlad merged commit 20703d06c2 into develop 2026-07-06 00:07:00 +03:00
Sign in to join this conversation.