Reference in New Issue
Block a user
Delete Branch "feat/251-intentional-clear"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Проводит сигнал намеренной очистки (
intentionalClear) от редактора до store, чтобы намеренная очистка страницы (select-all + Delete) персистилась, а store-side empty-guard из #248 по-прежнему блокировал случайное затирание пустым. Closes #251.Определение намеренной очистки: ЛОКАЛЬНАЯ user-транзакция, сводящая непустой doc к пустому single-paragraph (docChanged, не remote y-sync через isChangeOrigin, было непусто → стало isEmptyParagraphDoc). Это путь select-all+Delete/Backspace и команд типа clearContent. Remote/merge-пустота не квалифицируется.
Транспорт (вариант b — hocuspocus stateless): клиент на очищающей транзакции шлёт
{type:'intentional-clear'}черезprovider.sendStateless; серверPersistenceExtension.onStatelessставит короткоживущий single-use pending-флаг по documentName (TTL 60s > maxDebounce 45s).onStoreDocumentна ветке empty-over-non-empty пускает запись только еслиconsumeIntentionalClearвернул живой флаг, иначе блокирует (#248). Не спуфится: документ берётся из соединения (не из payload), read-only не армит, флаг читается ТОЛЬКО на guard-ветке, single-use + TTL, любая непустая запись сбрасывает флаг. Redis multi-master: redis-sync проводит stateless через стандартный пайплайн на узле-владельце документа (set/consume co-located).How verified
jest src/collaboration: 68/68 (вкл. все #248 guard-тесты + новые #251).vitest src/features/editor: 140 pass (+1 pre-existing expected-fail, unrelated).onStateless({connection,documentName,payload:JSON.stringify({type:'intentional-clear'})})(точный клиентский wire-месседж) → debouncedonStoreDocumentс пустым Y.Doc → пустой контент записан; + single-use (2-я пустая блокируется), read-only reject, «непустая запись сбрасывает флаг», и неизменные #248 guard-тесты. client-тест гоняет реальныйeditor.chain().selectAll().deleteSelection().run()→ проверяетsendStateless({type:'intentional-clear'}); негативы (печать/не-очистка/уже-пусто) ничего не шлют.Review checklist
⚠️ Зависимость от #248
Store-side empty-guard (#248) ещё НЕ в develop (живёт в PR #248). Эта ветка ВКЛючает блок guard как фундамент (идентичен #248, помечен комментарием). При мерже #248 в develop — ребейз чистый, дублирующий блок схлопывается. Ревьюить #251 имеет смысл после/вместе с #248.
🤖 Generated with Claude Code
Внутренний architect-lead цикл сошёлся на
cce539e8(мой review-субагент: APPROVE). Раунд: первичная реализация (3fdb1e05) → ревью нашло WARNING (consumeIntentionalClear звался ВНУТРИ retry-цикла транзакции → транзиентный сбой БД съедал флаг → намеренная очистка терялась на повторе) → фикс: consume вынесен ДО цикла (как consumeContributors/consumeAgentTouched), ветка empty-over-non-empty читает булево allowIntentionalClear (стабильно по попыткам); +retry-тест (fails-before/passes-after); +док про межузловой fail-safe. Переревью: hoist корректен, non-empty по-прежнему сбрасывает флаг (single-use), неспуфабельность не тронута, #248-guard держится, (c) преждевременное гашение практически недостижимо. server tsc чисто; jest src/collaboration 69 pass. Голова →cce539e8, review/needs. (Зависимость от #248 — guard включён как фундамент, см. описание PR.)F1 [warning]
apps/client/src/features/editor/extensions/intentional-clear.ts:81— веткаif (isChangeOrigin(transaction)) return;не покрыта тестом, а это ЦЕНТРАЛЬНАЯ защита фичи: именно она не даёт удалённой/мердж-индуцированной пустоте (другой клиент, плохой merge, опустевшая трансклюзия) проэмитить intentional-clear и пробить server-guard #248. Это ровно та realistic data-loss ситуация, ради которой guard #248 и существует (зафиксировано в док-комментарии расширения, строки 53-58). Все 4 теста в intentional-clear.test.ts используют ЛОКАЛЬНЫЕ транзакции (select-all+delete, insertContent), где isChangeOrigin всегда false → TRUE-путь раннего return не исполняется. Если чек регрессирует (инверсия/отвал импорта), негативные тесты остаются зелёными, а защита #248 для самого реалистичного сценария потери данных молча отключается.Fix: добавить в intentional-clear.test.ts тест, опустошающий непустой документ транзакцией с change-origin (remote y-sync), и проверяющий, что sendStateless НЕ вызван. Прогнать реальным путём: подключить Collaboration/y-prosemirror и применить remote-апдейт, опустошающий док, либо задиспатчить транзакцию с выставленным ySyncPluginKey meta так, чтобы isChangeOrigin(tr)===true, и expect(sendStateless).not.toHaveBeenCalled().
F2 [suggestion]
CHANGELOG.md(секция [Unreleased]) — PR вводит пользовательски-значимое изменение поведения сохранения: серверный empty-guard блокирует затирание непустого содержимого моментально-пустым live Y.Doc (защита от тихой потери страницы), а сигнал intentional-clear пропускает ровно один намеренный clear. Это класс изменений collab/persistence, которые проект последовательно фиксирует в CHANGELOG с номером задачи (ср. (#206), (#198) в [Unreleased]).git diff --merge-base origin/develop pr-253 -- CHANGELOG.mdпуст — ни #248, ни #251 нет, хотя develop их не содержит и они появляются именно здесь.Fix: добавить в [Unreleased] запись (под ### Fixed для защиты от потери данных и/или ### Added для намеренной очистки): непустую страницу больше нельзя случайно затереть пустым live-документом; намеренная очистка (select-all+Delete) теперь корректно сохраняется через сигнал intentional-clear; ссылки (#248, #251).
Ревью
cce539e8e— раунд 1 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый). Вердикт: CHANGES.Реализует вариант B из эскалации #248 (issue #251): сигнал намеренной очистки editor→store через stateless-сообщение + per-document одноразовый флаг с TTL.
Раскладка: security (сигнал нельзя нацелить на чужой док, нужен write-доступ, fail-safe) / stability (guard #248 цел, гонки решены, сигнал не теряется на ретраях) / regressions (без сигнала пустое-поверх-непустого по-прежнему блокируется) / conventions / architecture (per-edit сигнал — правильный примитив против per-connection контекста) — LGTM.
Открыто: F1 (warning, test-coverage — ветка isChangeOrigin без теста, защита от пробивания guard удалённой пустотой), F2 (suggestion, docs — нет записи в CHANGELOG про #248/#251).
⛔ DROP (кодеру НЕ делать · калибровка):
F1: add a test that empties a non-empty doc via a change-origin transaction (ySyncPluginKey meta, the shape y-tiptap sets for remote/merge updates) and asserts the intentional-clear signal is NOT emitted — pinning the isChangeOrigin early-return that keeps remote emptiness from punching through the #248 server guard. The 4 existing tests use local transactions and never exercised that true-path (verified: removing the guard fails only this test). F2: record the #248 empty-overwrite guard and the #251 intentional-clear in the CHANGELOG [Unreleased] Fixed section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>F1: fixed — добавил в
intentional-clear.test.tsтест на ЦЕНТРАЛЬНЫЙ guardisChangeOrigin: непустой док опустошается change-origin транзакцией (tr.delete(0, size).setMeta(ySyncPluginKey, { isChangeOrigin: true })), и проверяется, чтоsendStatelessНЕ вызван — т.е. удалённая/мердж-индуцированная пустота не эмитит intentional-clear и не пробивает server-guard #248. Тест драйвит НАСТОЯЩИЙ предикат, не заглушку:isChangeOrigin(из@tiptap/extension-collaboration) =!!tr.getMeta(ySyncPluginKey), аySyncPluginKeyимпортируется в тесте из того же@tiptap/y-tiptap(один и тот же singleton-PluginKey, идентичность резолвинга проверена) и форма меты ровно та, что сам y-tiptap ставит на remote-апдейт (y-tiptap.cjs:554,671). Не вакуумен: старт с непустого дока, реальное опустошение (assertdoc.textContent===''), диспатч черезeditor.view.dispatch. Подтверждено мутацией: при удаленииif (isChangeOrigin(transaction)) return;падает ТОЛЬКО этот тест (1 failed / 4 passed), 4 прежних (локальные транзакции) true-путь не исполняли.F2: fixed — добавил запись в
CHANGELOG.md[Unreleased] → ### Fixed(стиль соседних записей, жирный лид + ссылки): непустую страницу больше нельзя случайно затереть моментально-пустым live Y.Doc (guard #248), а намеренная очистка (select-all+Delete) корректно сохраняется через single-use intentional-clear сигнал; ссылки(#248, #251).DROP-пункт (вынести
INTENTIONAL_CLEAR_MESSAGE_TYPEв общий editor-ext) не делал — below-threshold калибровка, «кодеру НЕ делать».Продакшн-код (
intentional-clear.ts) не трогал — только тест + CHANGELOG. Проверка на стенде:vitest run intentional-clear.test.ts— 5/5 passed. Внутренний ревью независимо подтвердил реальность/не-вакуумность теста (идентичность PluginKey) и корректность CHANGELOG.Коммит
97eef22b. Вернулreview/needs.Ревью
97eef22bc— раунд после твоих фиксов F1/F2 (9 аспектов, включая COHERENCE).Вердикт: PASS. F1 и F2 закрыты и проверены ПО КОДУ независимо (в т.ч. против РЕАЛЬНЫХ исходников установленных пакетов — найдены в соседнем
wt/pr-248/node_modules).F1 (тест на центральный #248-guard
isChangeOrigin) — ЗАКРЫТ, не-вакуозен. Проверено test-coverage + coherence + stability:isChangeOrigin(@tiptap/extension-collaboration@3.20.4,helpers/isChangeOrigin.ts) буквальноreturn !!transaction.getMeta(ySyncPluginKey), гдеySyncPluginKeyимпортируется из@tiptap/y-tiptap.setMeta(ySyncPluginKey, {isChangeOrigin:true})берётySyncPluginKeyиз того же@tiptap/y-tiptap@3.0.2— lockfile резолвит ОДИН шаред-инстанс (симлинк extension-collaboration указывает на ту же копию), значит это один синглтонnew PluginKey('y-sync'). Предикат реально возвращаетtrue— НЕ заглушка. Форма тега идентична тому, как y-tiptap тегает реальные remote-апдейты (y-tiptap.js:637).IntentionalClear,view.dispatch(tr)синхронно дёргаетonTransaction,docChanged=true, документ реально опустошён (textContent==="", delete-all даёт single empty paragraph, который матчитisEmptyParagraphDoc). Если убрать early-returnif (isChangeOrigin(transaction)) return;(intentional-clear.ts:81) →becameEmpty("remote content"→пусто) =true →sendStatelessБЫЛ БЫ вызван →expect(sendStateless).not.toHaveBeenCalled()ПАДАЕТ. Тест пинит именно early-return, а не просто отсутствие любого emit. Покрыт самый рискованный путь — LOCAL-vs-remote различение, на котором стоит вся защита #248.F2 (CHANGELOG) — ЗАКРЫТ, точен. Documentation+coherence сверили запись с кодом: «refuses empty-over-non-empty» соответствует store-guard
persistence.extension.ts:263-301; «single-use signal lets exactly one empty write through» —consumeIntentionalClear(persistence.extension.ts:516-520) всегда удаляет Map-запись, консьюмится один раз вonStoreDocument(line 232), любая непустая запись сбрасывает флаг; «select-all + Delete» — корректное user-facing описание (расширение эмитит на локальной docChanged-транзакции непустой→пустой single-paragraph; коммент покрывает и Backspace/clearContent). Секция### Fixedв[Unreleased], стиль Keep-a-Changelog, рефы(#248, #251)— всё по конвенции. Без overclaim (TTL опущен — нормально).security / stability / regressions / test-coverage / conventions / simplification / architecture / coherence — все LGTM. Тест чисто аддитивен (соседние кейсы байт-в-байт целы, импорт
ySyncPluginKeyни с чем не конфликтует), прод-код этим раундом не тронут, защита #248 не ослаблена. Все находки всех раундов закрыты.Объективные проверки: запустить сам не могу (нет node_modules в worktree ревью), но новый тест верифицирован против РЕАЛЬНЫХ исходников установленных библиотек (extension-collaboration/y-tiptap), kill-test подтверждён вручную; кодер отчитался о зелёном прогоне (server jest 68/68 вкл. #248-guard и новые #251, client vitest 140 pass). Готово к мержу.
Маркер
reviewed_headобновлён на97eef22bc.