Files
houseplan-card/docs/reviews/CODE-REVIEW-243-r2.md
T
2026-08-22 16:58:34 +00:00

35 KiB
Raw Blame History

CODE-REVIEW-243-r2

  • Issue: #243 — «Перетаскивание вкладок пространств не работает: захват указателя съедает цель. Плюс нужен указатель места вставки»
  • Этап: code (PROCESS.md §2.7)
  • Заход: r2 · блокирующих циклов израсходовано 0 из 4 (r1 был зелёным и бюджет не тратил, §7.2, #227)
  • ТЗ: docs/specs/243-space-tab-drop-target.md (7ac1934, не менялся с r1), SPEC-REVIEW: docs/reviews/SPEC-REVIEW-243-r1.md, вердикт зелёный
  • Диапазон этого раунда: origin/dev...HEAD (origin/dev = 56a3977, дальше не двигался), 5 коммитов:
    • 133fdc9 docs(spec): define reliable space tab drop target (класс C, Issue: #243, User-Visible: no)
    • 02a5230 docs: review document for #243 (класс C, SPEC-REVIEW документ)
    • e6ac033 fix(tabs): restore captured mouse reordering (класс A/B/C/D, Issue: #243, User-Visible: yes) — единственный коммит с продуктовым поведением
    • 0fa7c28 docs: review document for #243 (класс C, CODE-REVIEW-243-r1 документ)
    • e4fe502 docs: accept screenshot fingerprint for #243 (класс C, Issue: #243, User-Visible: no)
  • Ревьюер этого раунда не имеет контекста реализации; материал — диапазон коммитов и diff, не рассказ автора.

Почему это r2, а не продолжение r1

Вердикт r1 (комментарий 2026-08-22T16:39:51Z, документ docs/reviews/CODE-REVIEW-243-r1.md) был зелёным, 0 находок, получен на implementation-коммите f4de527. SHA в том вердикте назван прямо в тексте документа — это не находка данного раунда.

После зелёного ревью слияние в dev конфликтовало (docs/images/screenshots.json). Владелец вернул issue в S6-in-progress только для ребейза (комментарий 2026-08-22T16:40:03Z), без указания на дефект кода. Автор перебазировал ветку на origin/dev 56a3977; implementation-коммит получил новый SHA e6ac033, добавился разрешающий конфликт коммит e4fe502 (принятие нового screenshot-фингерпринта из канонического CI-прогона).

Разбор в этом раунде полный, не по дельте — по прямому указанию PROCESS.md §2.10 и AGENTS.md: «после ребейза на ушедший вперёд dev это другой код», а не редактирование по замечанию. Диапазон origin/dev...HEAD целиком (все 5 коммитов) — предмет разбора, включая diff, который r1 уже видел под другим SHA, и диапазон, который r1 не видел вовсе (e4fe502).

Скоуп ревью

Изменённые файлы продуктового/тестового кода (коммит e6ac033, единственный с поведением):

src/houseplan-card.ts             78 +++--   pointer-move на .tabs, координатный hit-test,
                                              suppress-click, _endTabDrag() в _pickSpace/_setMode
src/styles.ts                      3 +-      drop-before / drop-after вместо .droptarget
demo/smoke_space_tab_reorder.mjs 497 (переписан на трастед page.mouse)
demo/golden/harness.mjs           68 ++      семантический guard для tabDrag-сцен
demo/golden/matrix.mjs             8 +-      2 новые сцены, версия матрицы 35 → 36
demo/golden/run.mjs                7 ++      release мыши в finally
scripts/mutation-gate.mjs         48 ++      3 новых мутанта + правка tab-drag-outlives-the-card
test/golden-matrix.test.mjs       23 ++      unit-контракт двух новых сцен
docs/CHANGELOG.md/.ru.md, docs/USER-GUIDE.md/.ru.md, docs/TESTING.md — в том же коммите

Плюс отдельным коммитом e4fe502: только docs/images/screenshots.json (source/имиджные хэши скриншотов), со ссылкой на канонический прогон Docs screenshots.

Как проверялось

  1. Прочитан docs/SCOPE.md — задача чинит регрессию выпущенной функции J6 («keep the plan true as the home evolves»), персона home admin, поверхность desktop-редакторы; новый разделитель делает уже принятый контракт #220 честным, мандат не расширяет.
  2. Прочитаны AGENTS.md и PROCESS.md целиком, включая §2.10 (объём повторного раунда), §7.2 (ребейз — другой код), §4 (лимит циклов), §8 (соразмерность гейтов), §12.
  3. Прочитано тело issue #243 и все комментарии, включая обе реплики автора о ребейзе и оба предыдущих вердикта ревьюера (SPEC-REVIEW r1 и CODE-REVIEW r1).
  4. Прочитан ТЗ docs/specs/243-space-tab-drop-target.md целиком (не менялся с r1) и сверен построчно с текущим diff по каждому разделу — контракт hit-testing §7, контракт завершения §8, визуальный контракт §9, AC1–AC9 §12.
  5. Прочитан docs/USER-GUIDE.ru.md/.md — формулировка про разделитель и сторону вставки согласована с текстом ТЗ, терминология не изобретена.
  6. Построчно прочитан весь diff src/houseplan-card.ts (не только «что изменилось с r1», а весь diff origin/dev...HEAD, поскольку это разбор с нуля): _tabPointerDown, новый _tabDropTargetAt, переписанные _tabPointerMove/_tabPointerUp, новый _suppressNextTabClick/_tabSuppressClickTimer, вызовы _endTabDrag() в _pickSpace/_setMode/disconnectedCallback, изменение шаблона рендера (@pointermove на контейнере .tabs, классы drop-before/drop-after).
  7. Самостоятельно (не со слов автора и не со слов r1) прослежена семантика двойной доставки pointerup: захват указателя ретаргетирует pointerup/совместимые mouse-события на исходную вкладку независимо от того, где физически отпущена кнопка; локальный @pointerup на исходной вкладке срабатывает раньше, чем событие всплывает до window, и синхронно снимает оконный листенер внутри _endTabDrag() до того, как всплытие туда дойдёт — поэтому _commitTabOrder вызывается не более одного раза на один физический pointerup. Оконный листенер остаётся единственным путём для случаев, когда локальный слушатель не сработал бы (потеря capture браузером).
  8. Самостоятельно вручную прогнана математика reorderSpaceIds (src/space-order.ts:55-65, диффом не тронут) на обоих направлениях переноса и сверена со знаком placement из _tabDropTargetAt: перенос влево (targetIndex < sourceIndex) даёt before и splice действительно ставит перетаскиваемую вкладку перед целью; перенос вправо даёт after и ставит её после — соответствует §7.3 ТЗ.
  9. Прочитан diff src/styles.ts — drop-before/drop-after, inset box-shadow с разным знаком, один theme token — соответствует §9 ТЗ.
  10. Прочитан diff demo/smoke_space_tab_reorder.mjs целиком (497 строк): все позитивные сценарии (AC1–AC4, включая негативный на View в строках 279–295) используют page.mouse; синтетический dispatchEvent остался только в двух местах — явный pointercancel (AC5, строка 228) и явная ветка pointerType: 'touch' (AC6, строка 271) — как и требует ТЗ §13.1.
  11. Прочитан diff demo/golden/harness.mjs/matrix.mjs/run.mjs, test/golden-matrix.test.mjs.
  12. Прочитан diff scripts/mutation-gate.mjs — три новых мутанта, плюс адаптация find/replace в tab-drag-outlives-the-card под новый порядок строк (_endTabDrag(); clearTimeout(...)), появившийся из-за _tabSuppressClickTimer.
  13. Прочитан diff docs/images/screenshots.json (коммит e4fe502) — только смена sourceFingerprint и по одному sourceSha256 на сцену; imageSha256 не изменились ни у одной сцены (0 переснятых PNG), коммит содержит ссылку на прогон канонической Docs screenshots джобы.
  14. Прочитаны трейлеры всех пяти коммитов и структура e6ac033 (единственного user-visible) — оба changelog, оба USER-GUIDE, TESTING.md в одном коммите с поведением.
  15. Прогнаны гейты лично (раздел ниже), не со слов автора и не переиспользуя цифры документа r1.

Гейты — что прогнано и почему

Обязательные всегда (PROCESS.md §8, §2.10 — дешёвые гейты гоняются в каждом раунде):

Гейт Команда Результат
Typecheck npx tsc --noEmit PASS, без вывода
Unit npm test PASS — 1091 tests, 1091 pass, 0 fail
Build + сверка бандлов npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js PASS, обе копии идентичны dist
Docs fingerprint node scripts/check-docs.mjs (обязателен, diff трогает src/**) PASS — «Documentation checks passed (7 files, 10 external links)»

По необходимости, определяемой diff/AC:

Гейт Команда Результат Почему прогнан
Smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD Прямое совпадение: smoke_space_tab_reorder. Слабая связь по общему _model (10 смоков) Обязателен для решения по выборке; вывод не изменился после ребейза
Целевой browser smoke node demo/smoke_space_tab_reorder.mjs OK, все 37 полей true Прямое совпадение из smoke-select, AC1–AC6
Mutation-gate, новые id node scripts/mutation-gate.mjs --id=tab-drag-target-follows-captured-source (и остальные два новых id по отдельности через --check, см. ниже) Пойманы Доказательство «тест умеет падать» для AC1/AC2/AC4
Mutation-gate, полный реестр node scripts/mutation-gate.mjs --check Все мутанты «ok», включая три новых (tab-drag-target-follows-captured-source, tab-drop-indicator-always-before, tab-drop-outside-commits-last-target) и адаптированный tab-drag-outlives-the-card Проверяет, что адаптация мутанта под новый порядок строк в disconnectedCallback не потеряла покрытие regression-риска #220
Golden verify (целевые сцены) npm run golden:verify -- --only=space-tab-drop-before-light,space-tab-drop-after-dark (флаг --only не ограничивает вывод — прогоняется весь набор) Обе новые сцены: missing-baseline, не error; в artifacts/golden/golden-report.json лично прочитан runtime.tabDrag каждой сцены: ok: true, trusted: true, правильные sourceId/targetId/placement, insetX положительный для before (1.14858) и отрицательный для after (-1.14935) AC2/AC8 требуют доказательства семантического guard на реальном page.mouse-жесте; проверено не по факту статуса missing-baseline, а по содержимому отчёта
Слабые связи smoke-select (по теме diff) node demo/smoke_fixed_floor.mjs, node demo/smoke_space_card.mjs, node demo/smoke_deeplink.mjs, node demo/smoke_kiosk_pan_lock.mjs Все PASS/OK fixedFloor/навигация вкладок задеты AC6; _pickSpace/_setMode получили новый вызов _endTabDrag()
Дополнительно по теме diff (не из smoke-select) node demo/smoke_modes.mjs OK _setMode — новая точка вызова _endTabDrag() в этом diff; смок не помечен smoke-select (нет прямого совпадения по идентификатору), но напрямую упражняет изменённый путь
Process-gate node scripts/process-gate.mjs --range origin/dev..HEAD «гейт пройден, предупреждений 0», 5 коммитов Дешёвый офлайн-гейт: трейлеры/класс файлов/ветка на весь диапазон раунда

Не прогонялось и почему:

  • smoke_audit_1490, smoke_glow_blending, smoke_render_perf, smoke_styling_hooks, smoke_virtual_light_toggle, smoke_zoom_out — слабая связь только по общему _model; diff их не трогает (подтверждено чтением diff), тема — рендер устройств/зум/тени, не путь tab-drag.
  • python -m pytest tests_backend — diff не затрагивает custom_components/**/*.py (подтверждено git diff --stat).
  • Performance-профиль — ТЗ §11 явно снимает требование (линейный проход по ≤50 пространствам), в AC не назван.
  • Полный npm run golden:verify по всей матрице как предмет построчного разбора — только то, что обе НОВЫЕ сцены дошли до missing-baseline, а не error; остальные ~74 «different»/«missing-baseline» сцены не относятся к #243 (диффом не менялись), это известный долг непринятых baseline на dev, предрелизная обязанность (Правило 13), не этого код-ревью.
  • npm run golden:accept — не выполнялся и не должен выполняться в этом цикле.
  • Реальный gh run view канонического Docs screenshots прогона (runs/32585516781) — не открывался; достаточным доказательством честности screenshots.json служит локально пройденный check-docs.mjs (сверяет фингерпринт с текущим src/** независимо от источника коммита) и то, что ни один imageSha256 не изменился — переснятых PNG нет, только пересчитан source-хэш.

Проверка AC

Полный повторный разбор (не по дельте относительно r1), так как раунд открыт ребейзом, а не находкой — см. «Почему это r2».

AC Итог Как доказано
AC1 Выполнен smoke_space_tab_reorder: realMouseMovedTabLeft, firstDropWroteOnce, sameTabMovedRight, secondDropWroteOnce — все true (лично прогнано на e6ac033); жест — трастед page.mouse, capture сохраняется (captureKeptButTargetResolved)
AC2 Выполнен Smoke: leftDropUsesBeforeSide/rightDropUsesAfterSide, beforeDividerHasPositiveInset/afterDividerHasNegativeInset; golden-отчёт лично прочитан: insetX +1.149/-1.149, placement совпадает с ожидаемым
AC3 Выполнен Smoke: subThresholdStayedClick, subThresholdDidNotWrite, clickStillSwitchesSpace
AC4 Выполнен Smoke: outsideClearsDropTarget, outsideReleaseEndedDrag, outsideReleaseDidNotWrite, nextClickWorksAfterOutsideRelease
AC5 Выполнен Smoke: pointerCancelEndedDrag/DidNotWrite, dragWasActiveBeforeDetach/detachEndedTheDrag/detachedCardDidNotWrite/orderSurvivedDetach; полный mutation-gate --check зелёный, включая адаптированный tab-drag-outlives-the-card (проверено, что адаптация под новую строку _tabSuppressClickTimer не ослабила мутант)
AC6 Выполнен Smoke: touchDidNotReorder, notReorderableInView/viewDidNotReorder, fixedFloorNotReorderable; отдельно прогнан smoke_fixed_floor — PASS; canStartTabDrag-гейт в _tabPointerDown/_canReorderTabs diff'ом не тронут (сверено чтением)
AC7 Выполнен, разобран по коду, не новым тестом Тело _commitTabOrder (src/houseplan-card.ts:1421-1450) не изменено этим diff — прочитано целиком и сверено с diff-хунками: единственная правка со стороны вызова — _tabPointerUp передаёт target.targetId вместо drag.overId; active-space/toast/маркеры подтверждены smoke (activeSpaceUnchanged, danglingMarkerPinnedToItsOldSpace, warnedAboutPositionalFloor/warningNotRepeated)
AC8 Выполнен artifacts/golden/golden-report.json лично прочитан для обеих новых сцен: tabDrag.ok: true для каждой, semantic guard (dragging/targetId/placement/знак inset/размер rect) прошёл на реальном жесте до сравнения с (отсутствующим) baseline; test/golden-matrix.test.mjs тест на структуру матрицы — в npm test
AC9 Выполнен Typecheck/unit/build/3-бандла зелёные лично; docs/CHANGELOG.md/.ru.md, docs/USER-GUIDE.md/.ru.md изменены в том же коммите e6ac033, что и поведение, с User-Visible: yes; process-gate зелёный на весь диапазон

Закрытие раунда r1

r1 закрылся зелёным вердиктом с нулём находок — таблицы «находка → чем закрыта» в обычном виде не требуется, потому что нечего закрывать содержательно. Единственное, что изменилось между r1 и r2, — техническое: ребейз ветки на новый dev и коммит с принятием нового screenshot-фингерпринта.

Что было в r1 Что произошло между r1 и r2 Где это видно
Implementation-коммит f4de527, зелёный вердикт, 0 находок Слияние конфликтовало на docs/images/screenshots.json; владелец вернул issue в S6-in-progress без указания на дефект кода, только для ребейза Комментарий issue 2026-08-22T16:40:03Z: «Код-ревью зелёное — вердикт выше в силе, переделывать работу не нужно. Не удалось только слияние»
— Ветка перебазирована на origin/dev 56a3977; тот же продуктовый diff получил новый SHA e6ac033; конфликт в screenshots.json разрешён в пользу канонического набора нового dev, добавлен коммит e4fe502 с новым source-фингерпринтом git show e6ac033 --stat (файлы идентичны списку из CODE-REVIEW-243-r1.md), git show e4fe502 (только screenshots.json, imageSha256 не изменились)
Диагноз/AC1–AC9/находки r1 Не изменялись правкой по замечанию — весь этот документ содержит самостоятельную повторную проверку каждого AC на e6ac033, а не перенос вывода r1 Раздел «Проверка AC» выше, гейты лично прогнаны заново на текущем HEAD

Других находок в r1 не было, поэтому таблица короче стандартного вида «находка | чем закрыта | где видно» — здесь нечего закрывать, кроме самого факта ребейза.

Унаследовано из r1

По правилу «ребейз — другой код» (PROCESS.md §2.10, §7.2) наследование выводов о самом implementation-диффе в этом документе не применялось — весь код e6ac033 прочитан и все гейты прогнаны заново лично, независимо от чисел и выводов docs/reviews/CODE-REVIEW-243-r1.md.

Унаследовано (не пересматривалось в этом раунде, так как относится к этапу, диффом не задетому):

  • ТЗ и его ревью. docs/specs/243-space-tab-drop-target.md (7ac1934) и docs/reviews/SPEC-REVIEW-243-r1.md — продуктовая рамка, скоуп/не-скоуп, формулировка AC1–AC9 не пересматривались повторно как предмет SPEC-ревью: файл ТЗ не менялся ни на ребейзе, ни в этом диапазоне (подтверждено — в diff origin/dev...HEAD нет хунка по docs/specs/243-*.md после начального коммита 133fdc9, который сам входит в этот диапазон и таким образом фактически перепрочитан в п.4 «Как проверялось», а не просто унаследован).
  • Математика #220 (reorderSpaceIds/applySpaceOrder в src/space-order.ts) — файл этим diff'ом не тронут; её корректность установлена ревью #220 и покрыта существующими unit-тестами, которые остаются зелёными (npm test, 1091/1091). Несмотря на это, семантика знака before/after из _tabDropTargetAt вручную сверена с ней заново в этом раунде (см. «Как проверялось», п.8) — это не чистое наследование, а самостоятельная перепроверка стыка нового и старого кода.
  • Канонический прогон Docs screenshots (runs/32585516781) — сам факт существования и «Chromium 151, 0 изменённых PNG» принят со слов коммита e4fe502 и подтверждён локально только через check-docs.mjs (фингерпринт совпадает) и через imageSha256, не через открытие лога прогона в Actions.

Находки

Находок нет — ни High, ни Medium, ни Low.

Отдельно проверенные риски ложной уверенности, снятые самостоятельным разбором этого раунда (не переносом выводов r1):

  • Не ослабила ли адаптация мутанта tab-drag-outlives-the-card под новый код регресс-контроль #220? Нет: find/replace теперь целится в блок this._endTabDrag(); clearTimeout(this._tabSuppressClickTimer); внутри disconnectedCallback — та же строка _endTabDrag() вырезается, только соседняя строка, под которую делается патч, сменилась из-за появления _tabSuppressClickTimer. Мутант лично прогнан в составе полного mutation-gate --check и помечен ok.
  • Не привёл ли ребейз к скрытому конфликту поведения (например, если между f4de527 и e6ac033 в dev появился код, трогающий тот же путь .tabs/_setMode/_pickSpace)? Нет: origin/dev сейчас — 56a3977, ровно тот SHA, на который ветка перебазирована согласно комментарию автора; других коммитов дальше нет (git log 56a3977..origin/dev — пусто), то есть весь diff, который я вижу, — это ровно послерейбейзный итог, без дополнительного скрытого дрифта.
  • Не подменяет ли check-docs.mjs (класс C, screenshots.json) продуктовую проверку — не относится к продуктовому поведению #243 напрямую, но входит в обязательный гейт docs (§8); диффом подтверждено, что изменение чисто конфликт-резолюшн (0 изменённых PNG), не попытка обойти гейт.
  • Риски, разобранные в r1 и переподтверждённые самостоятельно в этом раунде (двойная доставка pointerup, pointercancel — отдельный путь от pointerup, освобождение мыши golden-харнессом в finally, соответствие диагноза ТЗ фактическому фиксу через перенос pointermove на .tabs) — см. «Как проверялось», пп. 6–7, где вывод получен самим ревьюером этого раунда, а не цитированием r1.

Что проверено и корректно

  • Диагноз ТЗ (per-tab listener + retargeting после setPointerCapture фиксирует цель на источнике) устранён тем способом, который ТЗ §19 п.2 разрешал (обработчик на .tabs); подтверждено чтением и тем, что мутант, возвращающий старую адресацию, ловится смоком.
  • Координатный hit-test (_tabDropTargetAt) реализует правила §7 ТЗ: попадание по getBoundingClientRect(), исключение исходной вкладки, before/after по индексу в модели (не по геометрии — вручную пересчитано в этом раунде), сброс цели при промахе, устойчивость к wrap (rect конкретного элемента).
  • Визуальный контракт (drop-before/drop-after, inset box-shadow разного знака, один theme token) соответствует §9; подтверждено golden-отчётом (числовые значения insetX лично прочитаны).
  • Двойная доставка pointerup не воспроизводится по построенной самостоятельно модели событий: локальный листенер на захватившей вкладке снимает оконный листенер до того, как событие всплывает туда же.
  • _endTabDrag() в _pickSpace/_setMode — не расширение скоупа: прямо предписано контрактом завершения ТЗ §8 («смена режима/пространства... очищают drag»), покрыто smoke (modeChangeEndsDrag/modeChangeDidNotWrite) и дополнительно личным прогоном smoke_modes.mjs (OK) — путь _setMode, который получил новый вызов, не сломан.
  • Три новых мутационных guard'а (§14 ТЗ) индивидуально проверены как способные покраснеть; полный реестр mutation-gate --check зелёный целиком, включая адаптированный унаследованный мутант #220.
  • Оба changelog, docs/USER-GUIDE.md/.ru.md, docs/TESTING.md обновлены в одном user-visible коммите e6ac033; терминология («вкладка», «разделитель», «сторона вставки») согласована с ТЗ и USER-GUIDE, не изобретена.
  • Класс файлов и трейлеры на всех пяти коммитах диапазона — Issue: #243 везде, User-Visible: yes только на e6ac033; ветка issue/243-space-tab-drop-target; process-gate.mjs подтверждает 0 предупреждений на весь диапазон.
  • Коммит e4fe502 (принятие screenshot-фингерпринта) — генерируемый класс C, ссылается на реальный канонический прогон, не переснимал ни одного PNG, check-docs.mjs зелёный после него.

Чего не проверял

  • Полный npm run golden:verify по всем сценам построчно как предмет разбора — только то, что две НОВЫЕ сцены дошли до missing-baseline, а не error, и что их runtime.tabDrag.ok === true. Остальные «different»/«missing-baseline» сцены — известный долг непринятых baseline на dev, предрелизная обязанность, не задача этого код-ревью.
  • smoke_audit_1490, smoke_glow_blending, smoke_render_perf, smoke_styling_hooks, smoke_virtual_light_toggle, smoke_zoom_out — не прогонялись; связь со smoke-select только по общему _model, тема не пересекается с изменённым путём.
  • python -m pytest tests_backend — не запускался, backend не тронут этим диапазоном.
  • Performance-профиль — не запускался, не назван в AC, обоснование ТЗ §11 (≤50 пространств, линейный проход) проверено чтением, не измерением.
  • Лог канонического прогона Docs screenshots (runs/32585516781) в GitHub Actions — не открывался; доверие основано на локальном check-docs.mjs и неизменности imageSha256, а не на просмотре лога.
  • Ручное визуальное сравнение golden PNG для двух новых сцен — нет предмета: baseline ещё не принят и не должен приниматься в этом цикле.

Вердикт

Раунд открыт ребейзом на ушедший вперёд dev, а не находкой предыдущего ревью — r1 был зелёным с нулём находок. Согласно PROCESS.md §2.10/§7.2 («после ребейза это другой код») разбор проведён полностью и самостоятельно на текущем HEAD (e4fe502, implementation-коммит e6ac033), без опоры на числа документа r1: все девять AC заново доказаны — восемь автотестом (лично прогнанный browser smoke на трастед page.mouse, полный mutation-gate, golden semantic guard с лично прочитанным runtime.tabDrag, golden-matrix unit) и один (AC7) разобран чтением кода с явной пометкой «проверено чтением, не исполнением» для тела _commitTabOrder, которое diff не менял. Обязательные и относящиеся к diff'у дополнительные гейты прогнаны лично, включая один сверх списка r1 (smoke_modes.mjs, задет новой точкой вызова _endTabDrag() в _setMode). Единственный не-код коммит раунда (e4fe502, принятие screenshot-фингерпринта после разрешения конфликта) проверен: 0 переснятых PNG, ссылка на канонический прогон, check-docs.mjs зелёный. Находок нет.

Вердикт: зелёный · заход r2 · блокирующих циклов 0/4 · High: 0 · Medium: 0 → в задаче