19 KiB
CODE-REVIEW-381-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/381 — «Действие по нажатию: добавить "Ничего не делать"»
- Этап: код-ревью (PROCESS.md §2.7)
- Ветка:
issue/381-no-op-tap-action, ревизия на момент разбора0e95c063(приведена конвейером кdevдо ревью: поверх легло 1 коммитdev,f69a17cd→0e95c063— тот же код, другой SHA; переисполнение не требуется, разбор полный по правилам первого захода) - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- ТЗ:
docs/specs/381-no-op-tap-action.md(ревизия 2), ревью ТЗ зелёное (docs/reviews/SPEC-REVIEW-381-r2.md, заход r2, 1/4 циклов) — вопросов к контракту нет, разбирается его реализация.
Скоуп диффа
git diff origin/dev...HEAD (46 файлов): продуктовый код —
src/logic.ts, src/device-toggle.ts, src/houseplan-card.ts,
custom_components/houseplan/validation.py, четыре словаря i18n; тесты —
test/logic.test.mjs, test/device-toggle.test.mjs,
test/device-marker-polish-contract.test.mjs,
tests_backend/test_validation.py, demo/smoke_tap_ctx.mjs,
scripts/smoke-links.mjs; документация — docs/ARCHITECTURE.md,
docs/CONFIG-COMPATIBILITY.md, docs/USER-GUIDE.md, docs/USER-GUIDE.ru.md,
docs/TESTING.md, оба changelog, docs/specs/README.md; сгенерированное —
три копии бандла, docs/images/{01-view-desktop,06-device-editor}.png +
screenshots.json (пересъёмка из-за смены sourceFingerprint, весь src/**
затронут добавлением i18n-ключа). Единственная поверхность (Редактор
устройств → tap_action), без миграции конфига, без нового UX вне уже
одобренного контракта, без влияния на геометрию/толщину/layout.
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
green, без вывода |
| Unit-тесты | npm test |
1585 всего, 1584 pass, 0 fail, 1 skip — совпадает с числом хендоффа автора |
| Build | npm run build |
green, dist собран за 10.3 с |
| Сверка бандла | cmp dist/houseplan-card.js custom_components/.../houseplan-card.js, diff dist/houseplan-assets.json .../houseplan-assets.json, diff -rq dist/houseplan-assets .../houseplan-assets |
все три идентичны байт-в-байт |
| Docs fingerprint | node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 10 external links)» — обязателен, диф трогает src/** |
| no-new-any | node scripts/no-new-any.mjs --base origin/dev --head HEAD |
«Новых any нет» (9 добавленных строк в 3 файлах проверены) |
| Выбор смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
1 зарегистрированная связь: demo/smoke_tap_ctx.mjs ← DeviceTapAction, TAP_ACTIONS (see ниже) |
| Целевой browser smoke | npm run bundle:sync && node demo/smoke_tap_ctx.mjs |
все 9 проверяемых полей true (кроме ожидаемого virtInfo: "no-virt" на этом демо-фикстуре) |
| Мутационная проверка смока | вручную закомментирована строка if (action === 'none') return;, пересобран бандл, смок перезапущен |
смок упал на noneHasNoEffects: expected true, got false — тест умеет падать; после проверки файл восстановлен, git status чист |
| Bundle budget | npm run bundle:budget |
initial View 277959 B gzip / бюджет 300000 B — совпадает с числом хендоффа |
| Docs screenshots provenance | gh run view 33299523695 |
Скриншоты документации / workflow_dispatch / success — тот самый CI-прогон, из которого приняты PNG |
| CI на этом SHA | gh api repos/.../commits/0e95c063/check-runs |
лёгкая «Проверка (CI)» зелёная; полный смок-шард — skipped (не предрелизный прогон) — подтверждает вводную «зелёного Validate нет» |
| Backend targeted pytest | не прогонялся мной | нет pytest/homeassistant в этом окружении (нет .venv-backend); полагаюсь на запись автора: .venv\Scripts\python.exe -m pytest tests_backend/test_validation.py -q -k tap_action → 2 passed, плюс собственное построчное чтение изменения схемы (см. AC7 ниже) |
Не прогонялись и не требовались: npm run golden:verify (рендер маркера не
затронут — см. AC4), npm run invariants (геометрия/layout/open_spans/
толщина не затронуты), полный набор demo/smoke_*.mjs (205 файлов — только
диффу и AC соразмерен один целевой), performance-профили (AC9 не называет
влияния и bundle:budget уже в пределах бюджета).
Вывод smoke-select.mjs полностью:
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 4
Матрица: 205 смоков · порог «широкого» символа: больше 41 смоков
Зарегистрированная связь (1):
demo/smoke_tap_ctx.mjs
← DeviceTapAction, TAP_ACTIONS
the Device editor exposes the canonical ordered action list and the same
scenario proves explicit none consumes pointer and keyboard activation
without UI, feedback or HA side effects (#381)
Решение по строке: единственная зарегистрированная связь — тот же файл, что
уже прогнан целевым образом и провёл мутационную проверку. Других связей
(прямых или неопределённых) инструмент не назвал; расширять выборку не
требуется — диф ограничен одной поверхностью и не касается других consumers
tap_action (fingerprint/virtual-light/cover — не изменены, см. AC7 ниже).
Один номер — один источник: диф не вводит и не дублирует ни одной пользовательской величины (нет чисел, площадей, толщин, координат) — правило не применимо к этому изменению.
Разбор по AC (docs/specs/381-no-op-tap-action.md)
- AC1 — selector и persisted value.
TAP_ACTIONSрасширен до['info','more-info','toggle','run','none'](src/logic.ts:910), рендерится в этом порядке (houseplan-editor-runtime.ts:12346). Save пишет точный literal только когдаtapActionTouched(houseplan-editor-runtime.ts:7937-7940: return { tap_action: d.tapAction || null }), иначе сохраняет прежний lossless-контракт (originalTapAction) — Cancel не трогает_markerDialogвовсе. Доказано: unit (test/logic.test.mjs— порядок), smokefiveCurrentOptions/noLegacyOptions/defaultInfo= true. Выполнен. - AC2 — quiet no-op.
projectedTapActionвозвращает'none'только для точного literal (device-toggle.ts:463-473), не дляnull/''/unknown._clickDevice(houseplan-card.ts:5202-5209) резолвит актуальный marker по id, проецирует action и делаетreturnна'none'до guard/toggle/run/ info веток — то есть до_deviceBindingActive,_tapConfirm,_showToast,callService/callWS,_startDevicePressFeedback._keyDeviceвызывает тот же_clickDevice. Доказано: unitprojectedTapAction('none', …)='none'для light/switch; текстовый contract-тест (device-marker-polish-contract.test.mjs) фиксирует порядок операторов и отсутствие вызовов между проекцией и no-op веткой; browser smoke шпионит на все перечисленные side-effect-точки и подтверждает 0 вызовов +stopped/defaultPrevented= true для click/Enter/Space. Мутация (ручная, описана выше) доказывает, что смок умеет падать. Выполнен. - AC3 — независимые жесты.
_ctxDeviceи long-press таймер (houseplan-card.tsвне изменённого диапазона) не читаютtap_actionвовсе — диф не касается этих функций (0 строк изменено). Doказано чтением: ни_ctxDevice, ни_holdTimer-путь не входят в диф. SmokectxMoreInfo,ctxPrevented,editorNative= true. Выполнен. - AC4 — presentation parity.
device-presentation.ts(рендер face/pulse/badge/LQI/Glow/hover) ссылается наtapActionтолько для legacy'cover'(device-presentation.ts:264,331) — не тронут диффом и не получил новой ветки для'none'. Раз рендер структурно не читает новое значение, presentation не может измениться; проверено чтением, не исполнением (новый DOM-diff-тест не заводился — не нужен, так как нет кода, способного создать регресс). Выполнен, доказательство слабее заявленного в ТЗ «unit, DOM assertion» буквально, но эквивалентно по надёжности: change surface = 0 в файле, отвечающем за представление. - AC5 — defaults и legacy.
projectedTapActionне тронут в части absent/null/''/cover/unknown-веток — только добавлена явная проверка'none'в существующую цепочкуif. Существующие тесты на''/undefined/'future-action'/'cover'(device-toggle.test.mjs) не менялись и остаются green. Выполнен. - AC6 — соседние действия. Ветки
toggle/run/info/more-info(houseplan-card.ts:5217-5315) не изменены (диф в этом файле — ровно 3 строки, весь остальной код байт-в-байт как вdev). Toggle/Run-зависимые поля в редакторе остаются подeffectiveTapAction === 'toggle'/'run'(houseplan-editor-runtime.ts:12352-12428), блокcontrolsрендерится безусловно (:12430+) — подтверждает, что controls остаётся доступен дляnone, как того требует ТЗ. Выполнен. - AC7 — backend и transfer.
MARKER_SCHEMAдобавляет"none"вvol.Any(...)рядом с существующими literal иcover(validation.py:1693).test_every_tap_action_the_editor_offers_is_acceptedрасширен явной проверкой"none" in _ts_list("TAP_ACTIONS")и продолжает читать список изsrc/logic.ts, а не дублировать его — кросс-языковая проверка исключает рассинхронизацию.Marker.tap_actionостаётсяstring | nullбез сужения (types.ts:130), поэтому full/space transfer и duplicate-virtualization (не тронуты диффом — 0 строк) продолжают работать на прежнем generic-копировании поля; не проверял отдельным прогоном duplicate-virtualization fixture — диф её не касается и не может внести туда регресс (поле как былоstring | null, так и осталось). Backend pytest сам не прогонял (см. таблицу гейтов) — читаю изменение схемы: однострочное добавление в allow-list, риск ошибки минимален и синтаксически проверяем глазами. Выполнен с одной унаследованной (не самостоятельно перепроверенной) зависимостью — см. «Чего не проверял». - AC8 — i18n и docs.
tap.noneдобавлен в одном месте (послеtap.run) во всех четырёх словарях с точными переводами из ТЗ (RU «Ничего не делать», EN «Do nothing», DE «Nichts tun», FR «Ne rien faire»).USER-GUIDE.md/.ru.mdдобавляют строку таблицы и абзац про defaults;CONFIG- COMPATIBILITY.mdиARCHITECTURE.mdописывают literal, порядок операций и downgrade-границу — сверено построчно с кодом (см. AC2) и совпадает. Единственное отступление — новый параграф проnoneлёг в раздел «Legacy device tap action», хотяnoneне легаси; содержательно не вводит в заблуждение (раздел и так объединяет весьtap_action-compat), не требует правки — снимаю как Low без цикла. Выполнен. - AC9 — гейты и бюджет. См. таблицу выше: typecheck/test/build green,
no-new-anyчист,check-docsgreen, целевой backend pytest — авторская запись (команда+результат названы), browser smoke — green и подтверждённо падает на мутации, bundle budget в пределах (277959/300000 B), рост некритичен (диф добавляет один<option>и одну веткуif). Выполнен.
Находки
Нет High. Нет Medium (ни в скоупе, ни вне). Один Low снят без цикла:
- L1 (Low, снят без цикла).
docs/CONFIG-COMPATIBILITY.md: параграф про новый каноническийtap_action: noneнаходится под заголовком «Legacy device tap action», хотяnone— не legacy-токен, а новый canonical. Не искажает контракт (раздел и так единственное естественное место для всей tap_action-совместимости, соседний параграф проcover— настоящий legacy), поэтому правки не требую; фиксирую записью здесь.
Что проверено и корректно
- Порядок операций в
_clickDevice:stopPropagation→ drag/suppress/hold guard → resolve актуального marker по id →projectedTapAction→nonereturn — раньше любого capability/UI/HA-пути. Подтверждено чтением и мутационным прогоном смока. projectedTapActionразличает явный'none'от absence/null/''/ unknown — новых путей ложного срабатывания (truthy/falsy) не найдено.- Backend allow-list, i18n-паритет (4 словаря), user guides (EN/RU),
architecture/compatibility доки, оба changelog в одном
User-Visible: yes-коммите (07d3ee6b, трейлерыIssue: #381+User-Visible: yesподтвержденыgit show -s --format=full). - Три копии бандла синхронны байт-в-байт; docs-скриншоты пересняты и приняты
штатной джобой
Скриншоты документации(провереноgh run view), а не локальным захватом. - Смок
demo/smoke_tap_ctx.mjs— не декоративный: явно проверено, что он падает при устранении no-op-ветки.
Чего не проверял
- Полный
pytest tests_backend -q(и даже целевой-k tap_action) не прогонял сам — в этом окружении нетpytest/пакетаhomeassistantи нет.venv-backend. Опираюсь на запись автора (команда и результат «2 passed» названы явно, что процесс требует) плюс собственное построчное чтение однострочного изменения схемы. - Not-touched duplicate-virtualization и import/export код (0 строк в диффе)
— не искал функцию отдельно и не гонял её fixture: диф структурно не может
задеть её (
tap_actionкак был genericstring | null, так и остался). - Полный набор
demo/smoke_*.mjs(205 файлов) иgolden:verify— не запускал; диф ограничен одной нерендер-поверхностью,smoke-select.mjsназвал ровно один относящийся смок, presentation-код не тронут (AC4). - Орфография DE/FR переводов сверена только структурно (наличие ключа, соответствие ТЗ-строке); носителем языка текст не вычитывал.
- Ручную/браузерную проверку вне
demo/smoke_tap_ctx.mjs(например, реальный HA-инстанс) не делал — по правилам процесса фазы ручного тестирования нет, её роль здесь исполняет код-ревью и автотесты.
Вердикт
Все AC доказаны автотестом (с проверенной способностью падать) либо чтением кода в точках, где сам диф не создаёт новой ветки исполнения. High: 0, Medium: 0. Единственный Low снят без цикла с записью выше.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0 · Документ: docs/reviews/CODE-REVIEW-381-r1.md