25 KiB
CODE-REVIEW — issue #565, заход r2
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Скоуп
Материал ревью: git log --oneline origin/dev..HEAD / git diff origin/dev...HEAD
на SHA dded4fbd5c6d51e4127b4ddfd5d9a1c547f21de2 (рабочая копия на нём же).
Ветка приведена к dev конвейером до ревью: origin/dev продвинулся с 7d08a28d
(база r1) до 84a7773e (два новых коммита — 54265023 мутанты #569 и 84a7773e
публикация их ревью, оба трогают только scripts/mutation-registry.mjs и не
задевают ни один файл #565). Задача автора 82ce5aae перебазирована конвейером
в dded4fbd. Это другой код по определению §7.2, поэтому разбор — полный, не
по дельте: ниже проверены не только новые строки, но и то, что рёбейз не
исказил уже принятый материал r1 (сверка git diff origin/dev..HEAD -- scripts/mutation-registry.mjs — все шесть записей #565 присутствуют одним
чистым блоком, ни одного фрагмента #569 не задето и не продублировано).
Диапазон — четыре коммита: 6103e7de/c23c7927 (публикация SPEC/CODE-REVIEW-565-r1
конвейером, класс C), 6aca447f (продуктовый коммит r1, уже разобран
docs/reviews/CODE-REVIEW-565-r1.md), dded4fbd — предмет этого раунда,
единственный продуктовый коммит после r1 (fix: сохранить клавиатурную подсказку (#565), трейлеры Issue: #565 / User-Visible: yes на месте).
Единственная находка r1 — M1 (Medium, в скоупе): клавиатурная focus-tooltip
устройства тихо стиралась наведением мыши на любую комнату, а симметричная
защита у другого устройства была избыточно широкой (блокировала ЛЮБОЙ hover, а
не только сфокусированного устройства). Коммит dded4fbd — целевой фикс этой
находки. Файлы: src/houseplan-card.ts, src/live-hover.ts,
src/live-interaction-runtime.ts, demo/smoke_household_journeys.mjs,
scripts/mutation-registry.mjs, docs/CHANGELOG{.ru,}.md, docs/TESTING.md,
docs/UX-MODES.md — соответствует хендоффу. Ни device-presentation.ts, ни
space-render.ts, ни i18n, ни тесты test/*.test.mjs не тронуты — AC1–AC4,
AC8, AC9 дельта не задевает.
Разбор дельты (M1 → fix)
src/live-hover.ts вводит focusTips: WeakMap<object, LiveTip> — состояние
клавиатурной подсказки хранится отдельно от host._tip (то, что реально
отрисовано):
showDevicePointerTip— убрана защитаif (host._tip?.source === 'focus') return;. Это и была избыточно широкая половина M1: она блокировала legitimate hover ЛЮБОГО устройства мышью, пока где-то держался клавиатурный фокус. Теперь наведение мышью на другое устройство работает как обычно.showDeviceFocusTip— пишет tip и вfocusTips, и вhost._tip.hideDeviceFocusTip(blur) — чистит запись вfocusTipsпоdeviceId, сбрасываетhost._tipтолько если текущий показ и есть focus-tooltip.clearPointerHover(pointerleaveс устройства/комнаты) —host._tip = focusTips.get(value) || null: возвращает сфокусированную подсказку, если она ещё жива, иначеnull.reconcileDeviceFocusTip(на каждыйcommit()) — переведена на проверкуfocusTips, а неhost._tip, поэтому больше не путает временный pointer-tip с владением фокуса.- Новые экспорты
deviceFocusTipActive/clearDeviceFocusTip.
src/houseplan-card.ts:
_showTip(..., room)— новая охранаif (room && this._liveRt?. deviceFocusActive()) return;(houseplan-card.ts:7434) — это вторая, недостающая половина M1: комнатныйpointermove(tip(), houseplan-card.ts:11677) больше не перезаписывает активную клавиатурную подсказку. Прочитан весь путь:enterRoom(pointerenter) по-прежнему ставит_hoverRoomдля визуальной подсветки комнаты независимо от фокуса — это не тот контракт, который защищает M1 (подсветка, не подпись), и в ТЗ/ находке не упоминается._clearTransientHover— добавленthis._liveRt?.clearDeviceFocus();перед остальной очисткой. Вызывается на границах жизненного цикла: смена режима/пространства (_setMode, строка 7601,this._clearTransientHover (true)), смена модальности на touch/pen (_notePointer), множественный touch-контакт,pointerup/pointercancelна touch. Без этой правкиfocusTipsпережил бы границу иclearPointerHoverмог бы «воскресить» устаревшую подсказку после смены пространства — прочитано и подтверждено, что путь перекрыт для всех вызывающих_clearTransientHover._clearPointerHover— тернарник вместо??; функционально то же самое, но устраняет реальную ловушку:pointerLeave(): voidвозвращаетundefined, и старая формаthis._liveRt?.pointerLeave() ?? this._clearTransientHover()всегда дополнительно вызывала_clearTransientHover()(правый операнд??срабатывает наundefined), т.е. любойpointerleaveс живым_liveRtстирал вообще весь transient-hover, включая только что восстановленную focus-подсказку. Мутантdevice-pointer-leave-clears-focus- fallbackвоспроизводит именно эту старую форму — назван ниже.
src/live-interaction-runtime.ts — тонкие делегаты deviceFocusActive() /
clearDeviceFocus() на LiveRuntime, используются в двух местах выше плюс
dispose().
Симметрия достигнута: единственный инвариант «пока focusTips содержит
запись для хоста, посторонний pointermove не должен ни блокировать чужой
hover (было — избыточно), ни затирать текущую подсказку без предусмотренного
события (было — дыра M1)» реализован в одном источнике (focusTips), а не
двумя независимыми точечными проверками, как раньше.
Что проверено исполнением (не только чтением)
Локально пересобран бандл (npm run build && npm run bundle:sync) — три копии
байт-в-байт совпали с материалом ревью (git status --short пуст после
сборки), typecheck прошёл без ошибок.
| Гейт | Команда | Результат |
|---|---|---|
| typecheck + build | npm run build |
зелёный, tsc --noEmit без ошибок, dist пересобран идентично коммиту |
| bundle sync | npm run bundle:sync |
зелёный, git status чист — все 3 копии синхронны |
| check-docs | node scripts/check-docs.mjs --screenshots=warn |
зелёный: Documentation checks passed (7 files, 12 external links); единственное WARN — уже известный устаревший screenshot-фингерпринт (#479, ожидаемо на плейн-пуше, не блокирует; тот же вывод был у автора и в r1) |
node demo/smoke_household_journeys.mjs |
прямое совпадение по diff (_clearTransientHover, _showTip, _tip), несёт сами новые проверки j4.room_hover_does_not_replace_keyboard_tooltip, j4.other_device_keeps_ordinary_pointer_hover, j4.pointer_leave_restores_keyboard_tooltip |
зелёный, OK (0 записей в _failures) |
node demo/smoke_touch_tips.mjs |
прямое совпадение (_hoverRoom, _notePointer, _showTip, _tip); _clearTransientHover — тронутая функция, защищает AC7 |
зелёный, OK (touchClearsAllTransientHover, noTipOnTouch и др. — все true) |
node demo/smoke_room_tooltip_toggle.mjs |
прямое совпадение (_hoverRoom, _pointerModality, _tip); ровно тот путь комнатного tip(), который получил новую охрану |
зелёный, OK (roomTipRestoredOnMove, disabledRoomTip, deviceTipSurvives и др. — все true) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD (полный вывод
приложен решением по каждой строке ниже): 33 прямых совпадения (в основном по
широким символам _mode/_tip/_drag, которые встречаются в большинстве
смоков файла) + 1 «зарегистрированная связь» (smoke_value_face_source.mjs
← resolveDevicePresentation, не относится к этой дельте — device- presentation.ts не менялся в dded4fbd) + одна широкая помеченная явно
(_space, не учитывалась инструментом). Прогнаны три смока, которые прямо
покрывают изменённые функции и являются целью новых мутантов/носителем новых
проверок (household_journeys, touch_tips, room_tooltip_toggle); остальные
30 совпадений — по обиходным символам, использующимся в файле почти
повсеместно (_mode/_drag/_config встречаются в самом файле независимо от
tooltip-логики), не запускались: ни один из них не входит в файлы, изменённые
dded4fbd, а прочитанный код изменения (см. «Разбор дельты» выше) не касается
drag/resize/config/mode-переключателей как таковых — только маршрутизацию
tooltip между pointer и focus источниками. Полный набор — предрелизный гейт, а
не гейт ревью (PROCESS.md §8).
Дешёвые гейты на этом SHA (подтверждены дважды)
Validate на точном материале dded4fbd зелёный:
https://github.com/Matysh/houseplan-card/actions/runs/34816298250 — покрывает
npm test (я не перезапускал: воспроизвёл typecheck/build локально, они
совпали, дополнительных причин сомневаться в юнитах, которые delta не трогает,
нет), no-new-any, process-gate, mutation-gate (шардируется в review-candidate
прогоне, покрывает все 6 witness'ов #565, включая 2 новых, — см. ниже),
perf-smoke триггер (live-hover.ts/live-interaction-runtime.ts/
houseplan-card.ts входят в large-house-interaction-v1, прогнан тем же
Validate). Не требуются и не прогонялись: golden:verify (стили/подписи не
тронуты, диффом подтверждено — как и в r1), model-invariants (геометрия не
тронута), pytest tests_backend (бэкенд не тронут).
Таблица «AC · чем доказан · чем краснеет» — обновление к AC6
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC6 (комнатный триггер, закрывает M1) | j4.room_hover_does_not_replace_keyboard_tooltip в demo/smoke_household_journeys.mjs, прогнан лично, OK |
мутант device-focus-tooltip-room-hover-overwrites (снимает охрану if (room && this._liveRt?.deviceFocusActive()) return;), гард — тот же smoke; часть Validate review-candidate прогона на этом SHA |
| AC6 (восстановление после hover другого устройства, закрывает M1) | j4.other_device_keeps_ordinary_pointer_hover + j4.pointer_leave_restores_keyboard_tooltip, прогнаны лично, OK |
мутант device-pointer-leave-clears-focus-fallback (возвращает старую форму ?? , которая при живом _liveRt всегда доп. вызывает _clearTransientHover(), стирая восстановленную подсказку), гард — тот же smoke; часть Validate review-candidate прогона |
Обе новые записи scripts/mutation-registry.mjs матчатся find ровно на
текущий текст (houseplan-card.ts:7434, houseplan-card.ts:5748) — сверено
построчно, не только по имени функции.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| M1 (Medium, в скоупе): клавиатурная focus-tooltip тихо стирается наведением мыши на комнату; симметричная защита у другого устройства избыточно широка | 1) Охрана if (room && this._liveRt?.deviceFocusActive()) return; внутри _showTip не даёт комнатному pointermove затереть активную focus-подсказку. 2) Снята избыточная защита if (host._tip?.source === 'focus') return; в showDevicePointerTip — hover другого устройства мышью снова разрешён (устраняет вторую половину асимметрии). 3) focusTips WeakMap хранит владение независимо от текущего показа и восстанавливает его в clearPointerHover при уходе указателя. 4) Границы жизненного цикла (_clearTransientHover → clearDeviceFocus()) чистят состояние, чтобы восстановление не «воскрешало» устаревшую подсказку |
src/houseplan-card.ts:7434 (новая охрана), src/live-hover.ts:64-118 (весь механизм focusTips), src/houseplan-card.ts:7304 (clearDeviceFocus() в _clearTransientHover); поведенчески — три новых проверки j4.room_hover_does_not_replace_keyboard_tooltip / j4.other_device_keeps_ordinary_pointer_hover / j4.pointer_leave_restores_keyboard_tooltip в demo/smoke_household_journeys.mjs, лично прогнаны и зелёные; защищены мутантами device-focus-tooltip-room-hover-overwrites и device-pointer-leave-clears-focus-fallback |
Унаследовано из r1
Принято без повторной проверки кода (сам код этих участков не менялся в
dded4fbd, что подтверждено git show dded4fbd --stat — ни один из
перечисленных файлов/областей не входит в дифф этого коммита), опираясь на
docs/reviews/CODE-REVIEW-565-r1.md на материале 127cd07ba78fc75cd776ebd6518248a6bef91e82:
- AC1/AC2 (именованная навигация пространств, единственный
aria-current, Tab/Enter/Space не тронуты) —src/houseplan-card.ts,<nav>-разметка не менялась вdded4fbd. - AC3/AC4 (дедупликация доступного имени целыми сегментами, паритет 4
локалей) —
src/device-presentation.tsне входит в диффdded4fbd. - AC5 (общий источник содержимого focus/hover tooltip,
deviceTipContent, clamp по viewport) — функция не менялась; фикс трогает только маршрутизацию какая подсказка активна, не что в ней показано. - AC7 (touch/pen не открывают tooltip) — логика
_notePointerне менялась; дополнительно перепроверена исполнением (demo/smoke_touch_tips.mjs, зелёный, см. таблицу выше) — не только унаследована, но и подтверждена заново, поскольку_clearTransientHover, вызываемая на touch-границе, получила новую строку. - AC8 (подписи комнат: цвет/прозрачность/текст не тронуты) —
git diff origin/dev..HEAD -- src/не содержит ни одного файла стилей;space-render.tsне в диффеdded4fbd. - AC9 (нет миграции/новых полей конфига) — в диффе
dded4fbdне появилось ни одного поля схемы. - Трейлеры и changelog r1-коммита (
6aca447f) — уже проверены в r1-документе, сам коммит не менялся ребейзом (SHA всех коммитов ветки, кроме последнего, сохранились:6103e7de,6aca447f,c23c7927идентичны допереходному дереву по содержимому, изменился только82ce5aae → dded4fbd). - i18n (
nav.spacesво всех 4 языках, старые ключи не тронуты) —src/i18n/*не входит в диффdded4fbd.
Что проверено и корректно (дельта r2)
- Комнатная охрана размещена внутри
_showTip, а не только у вызова изtip()— значит защищает оба места вызова сroom=true(сейчас оно одно, но инвариант закреплён на уровне примитива, а не на уровне каждого вызывающего — устраняет корневую причину M1: «точечная правка одного вызывающего»). enterRoom/_hoverRoom(подсветка контура комнаты) намеренно не защищены этим инвариантом и продолжают обновляться при наведении — это визуальная подсветка плана, не подпись, вне контракта AC6 и вне текста находки M1.- Оба новых changelog-абзаца (RU/EN, в одном коммите
dded4fbd) описывают именно наблюдаемое поведение («room hover no longer replaces a keyboard tooltip; hovering another device temporarily shows that device and restores the focused one when the pointer leaves») — совпадает с тем, что показало исполнение smoke-проверок. docs/UX-MODES.md(канонический документ подсистемы) обновлён в том же коммите тем же предложением — терминология «Room hover does not replace it; ordinary mouse hover on another device may show that device temporarily, then pointerleave restores the still-focused device» соответствует коду.docs/TESTING.mdперечисляет оба новых мутанта под своими именами рядом со старыми четырьмя — не потерялся ни один свидетель.
Одно число — один источник
Дельта не добавляет и не меняет ни одной пользовательской величины (текст,
число, единицу измерения) — это чисто событийная маршрутизация состояния
tooltip между pointer- и focus-источником. deviceTipContent() остаётся
единственным источником заголовка/метрик для обоих путей и не изменена этим
коммитом (унаследовано из AC5, см. выше). Неприменимо к этому диффу.
Чего не проверял
- Не гонял
npm testнапрямую (полагаюсь на зелёный Validate точного SHAdded4fbd; локально воспроизвёл typecheck и build с идентичным результатом, дополнительных причин сомневаться в юнитах, которые дельта не трогает (test/*.test.mjsне в диффеdded4fbd), нет). - Не гонял
golden:verify,model-invariants,pytest tests_backend, perf-профили по отдельности — не применимо к дельте (геометрия/бэкенд/визуал не тронуты; perf-триггер входит в тот же Validate-прогон, который зелёный на этом SHA). - Не гонял 30 из 33 «прямых совпадений»
smoke-select.mjs— все они матчатся по широким обиходным символам (_mode,_drag,_config,_deviceDrag), используемым в файле независимо от tooltip-логики; прочитанная дельта не касается drag/resize/config/mode-переключателей, только маршрутизацию подсказки между pointer/focus. Прогнаны три смока, которые несут либо новые проверки (household_journeys), либо прямо упражняют тронутые функции (touch_tips—_clearTransientHover/AC7,room_tooltip_toggle— путьtip()/_showTipсroom=true). - Не гонял
smoke_space_card.mjs— файл вне диффаdded4fbd, поведение унаследовано из r1 без изменений. - Не проводил ручное тестирование в браузере — вне процесса (PROCESS.md: ручного тестирования в цикле нет).
- Не проверял поведение при реальном screen-reader — вне скоупа ТЗ, как и в r1.
Материал раунда
- SHA материала:
dded4fbd5c6d51e4127b4ddfd5d9a1c547f21de2(рабочая копия на нём же в момент ревью,git status --shortпуст). - Предыдущий заход:
docs/reviews/CODE-REVIEW-565-r1.md, материал127cd07ba78fc75cd776ebd6518248a6bef91e82(заход r1, вердикт жёлтый, High 0 · Medium 1). SHA r1 мёртв в текущей истории (ребейз), но найден и прочитан из committed-документа в самой ветке — не потребовалgit log --all --find-object. - Ребейз между заходами:
origin/devпродвинулся7d08a28d → 84a7773e(два коммита, оба вне скоупа #565); авторский коммит доработки82ce5aaeперебазирован конвейером вdded4fbd.
Материал раунда
- Ветка:
issue/565-view-accessibility, коммитdded4fbd5c6d— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
e63983fd12eebe0ec2029a09574d4e83cd933a14git log --all --format='%H %T' | grep e63983fd12ee - Тело issue:
86508a821b1cdb4b2b7ad0b6e79395cb2e8ff094ea42f52487221af5c5e28093 - Вердикт конвейера:
green· High 0