15 KiB
CODE-REVIEW-385-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/385
- Этап: code (PROCESS.md §2.7)
- Заход: r1 (первый цикл код-ревью для этого issue) · блокирующих циклов израсходовано 0 из 4
- ТЗ:
docs/specs/385-audit-lows.md, ревизия 2 (принята SPEC-REVIEW-385-r2, зелёный,687b2966) - Коммиты в диапазоне
origin/dev..HEAD:353724b2fix: audit-lows batch from the v1.69.0 audit (#385) — User-Visible: yes, Issue: #385564bf419build: refresh bundle trees for #385 — User-Visible: no, Issue: #385
Скоуп
Четыре точечные правки по ТЗ rev2:
- (а)
src/houseplan-editor-runtime.ts— клик по уже выбранному binding-кандидату (список HA-сущностей, :12305-12313) и повторный выбор уже активного virtual-режима (:12252-12260) становятся no-op:valueSource/valueSourceTouchedи бейдж не трогаются, сбрасывается толькоbindingOpen. - (б)
src/devices.ts—rewriteMarkerControlReferencesбольше не сажаетvalue_badge: undefined/value_source: undefinedусловным спредом. - (в)
scripts/process-gate.mjs— предикат релизности вынесен вisReleaseCommit(subject, one)(единственная копия выражения, оба дизъюнкта);parseRecordsвызывает дорогойreleaseSourceViolationsOfтолько когдаisReleaseCommitистинен для этого коммита. - (г)
custom_components/houseplan/import_export.py— параный комментарий у обоих форматов обезвреживания (value_badgeполями,value_sourceудалением ключа) + объединяющий pytest.
Плюс тесты (test/devices.test.mjs, test/process-gate.test.mjs,
tests_backend/test_ha_import_export.py), два новых мутанта
(scripts/mutation-gate.mjs), правка demo/smoke_value_face_source.mjs,
ченджлоги, обновление трёх копий бандла.
Как проверялось
Материал — git log --oneline origin/dev..HEAD и
git diff origin/dev...HEAD построчно по каждому изменённому файлу
(src/devices.ts, src/houseplan-editor-runtime.ts, scripts/process-gate.mjs,
custom_components/houseplan/import_export.py, все тестовые файлы,
scripts/mutation-gate.mjs, ченджлоги). Дополнительно чтением проверено
окружение диффа: полный список мест, где _valueBadgeForBinding и
d.binding = встречаются в houseplan-editor-runtime.ts (grep), чтобы
исключить третий путь выбора binding, который спека просила
перепроверить отдельно (раздел «Риски» ТЗ) — найдено ровно два места
выбора значения (:12257/:12310), оба в диффе; третье найденное вхождение
(:12275-12280, радио «from HA») не присваивает binding конкретному
значению и не трогает valueSource ни до, ни после диффа — вне контракта (а).
Гейты прогнаны лично на 564bf419 (зелёного Validate на этом SHA не было):
| Гейт | Результат |
|---|---|
npx tsc --noEmit |
чисто |
npm test |
1587 tests, 1586 pass, 0 fail, 1 skipped — совпадает с заявленным автором |
npm run build + сверка трёх копий бандла |
git status --short пуст после билда — дерево уже актуально |
node scripts/check-docs.mjs |
"Documentation checks passed (7 files, 10 external links)" |
npm run bundle:budget |
initial View 277 979 B / 300 000 B, headroom 22 021 B — совпадает с заявленным (+28 Б от базы) |
node scripts/mutation-gate.mjs --id=same-binding-click-resets-source |
поймано 1/1 |
node scripts/mutation-gate.mjs --id=release-proof-computed-for-every-commit |
поймано 1/1 |
node demo/smoke_value_face_source.mjs (после npm run bundle:sync, локальный demo/srv/assets не в git) |
все 16 полей true, включая sameBindingKeepsSource/sameVirtualKeepsSource (AC1/AC2) и регресс-ветку bindingResetToAuto |
python3 -m pytest tests_backend/test_ha_import_export.py -k 385 |
1 passed (AC5) |
python3 -m pytest tests_backend/ -q (полный, т.к. тронут import_export.py) |
446 passed, 1 skipped, 1 error — см. «Не проверял/не относится» |
node --test test/single-source-numbers.test.mjs |
3/3 — диф не добавляет новую видимую величину, гейт для полноты |
node scripts/smoke-select.mjs --base origin/dev --head HEAD: 2 файла в src/**,
1 символ на изменённых строках (_markerDialog), матрица 205 смоков, порог
широкого символа 41. Вывод — НЕОПРЕДЕЛЁННОСТЬ: только слабая связь по
общему имени _markerDialog (33 смока, включая уже прогнанный
smoke_value_face_source.mjs). Остальные 32 не прогонялись: связь слабая
(общее имя состояния диалога маркеров, а не конкретно binding-клика),
smoke_value_face_source.mjs — прямое совпадение (это тот самый смок,
который автор расширил под AC1/AC2) и уже покрывает сценарий диалога
целиком (открытие/выбор/сохранение/переоткрытие/статическая карточка/tap).
Инварианты модели (npm run invariants) не прогонялись — диф не трогает
рёбра комнат, толщину стен, layout, marker.space, open_spans.
npm run golden:verify не прогонялся — диф не меняет рендер/геометрию/стили
(логика диалога, отбрасывание ключа, гейт-скрипт, экспорт-нейтрализация).
Находки
Блокирующих (High) находок нет. Medium в скоупе — нет.
L1 (Low, не блокирует, снимаю записью). scripts/process-gate.mjs:118-121
и :166 — предикат релизности (isReleaseCommit) действительно один и тот же
и переиспользуется (AC4 выполнен буквально), но вспомогательный one(name)
продублирован двумя разными реализациями: в makeCommit — через
all(name)[0] ?? null на matchAll(/…/gmi), в parseRecords — через
text.match(/…/mi). Поведенчески идентичны (обе находят первое
^Name:\s*(.+)$ без учёта регистра), и это подтверждено новым тестом
(test/process-gate.test.mjs — коммит-бета с Release:-трейлером и
стабильный релиз по subject дают одинаковую классификацию в обоих вызовах).
Риск чисто будущий: если кто-то поменяет формат трейлера в одном one, не
тронув другой, isReleaseCommit в parseRecords и в makeCommit разойдётся
по входу, а не по логике предиката — сам предикат от этого не расходится
(это то, что просило AC4), но результат классификации коммита — да. Не в
скоупе AC4 (там речь про предикат, не про хелпер one), масштаб — рефакторинг
не по этой задаче. Оставляю на усмотрение автора.
Что проверено и корректно
- (а) — оба места выбора binding в скоупе, третьего нет (проверено grep +
чтением, см. «Как проверялось»); ранний no-op сохраняет весь драфт кроме
закрытия списка, что буквально соответствует контракту ТЗ. Смок гоняет и
успешный сценарий (сохранение source, предпросмотр, статическая карточка,
реакция на
unavailable), и обе новые ветки (а), не разрывая старую регресс-веткуbindingResetToAuto. - (б) — условный спред верен: ключ появляется в выходном объекте только
когда исходное значение (
marker.value_badge/marker.value_source) было задано или было переписано на новый ref; для marker без ключаvalueBadge/valueSourceостаютсяundefinedиз: marker.value_badgefallback → ключ не создаётся. Тестtest/devices.test.mjsбьёт по обеим веткам (голый маркер, маркер с существующимvalue_source). - (в) — семантика гейта не изменена (для нерелизных
nullкак и раньше,() => nullбольше не нужен — сравнение по значению, не по идентичности функции), дорогая проверка теперь гарантированно вызывается только для тех же коммитов, для которыхisReleaseистинен, потому что это буквально одна и та же функцияisReleaseCommit, вызванная дважды с однимsubject. Тест воспроизводит именно контрпример из ТЗ (бета-приёмка сRelease:трейлером — второй дизъюнкт). - (г) — асимметрия форматов задокументирована на месте,
dropped_marker_linksсчитает оба пути, pytest пришпиливает оба формата одновременно (не по отдельности, как раньше было бы недостаточно для AC5). - Ченджлоги (
docs/CHANGELOG.md,docs/CHANGELOG.ru.md) правлены в том же коммите353724b2, что и код, запись только про (а), соответствуетUser-Visible: yesи разделу «Release-артефакты» ТЗ. ТрейлерыIssue:иUser-Visible:на месте на обоих коммитах диапазона. - Три копии бандла (
dist/,custom_components/houseplan/frontend/,houseplan-assets.json×2) синхронны с исходниками — билд с нуля не дал diff после коммита564bf419.
Чего не проверял
- Полный browser-smoke матрицы (205 файлов) — не прогонял; инструмент дал
только слабую связь по общему имени
_markerDialog, прямое совпадение (smoke_value_face_source.mjs) прогнано и покрывает сценарий диалога целиком. Остальные 32 «слабых» смока не трогают binding-клик по названию и по факту (проверены выборочно по имени —smoke_binding_picker.mjs,smoke_binding_ui.mjs,smoke_dialog_zombie.mjsпро открытие/закрытие диалогов и выбор HA-сущности, не про повторный клик по уже выбранной); полный прогон матрицы — предрелизная обязанность, не гейт этого ревью. npm run golden:verify— диф не меняет рендер/геометрию/стили/слои.npm run invariants— диф не трогает геометрию модели (рёбра, толщину,layout,marker.space,open_spans).- Perf-профили — не названы в AC; (в) — только уменьшение работы гейта, ТЗ прямо говорит «бенч не требуется».
tests_backend/test_ha_upload.py::test_upload_ok— упал сAssertionErrorна проверке типа daemon-потока при остановке aiohttp-сервера (_run_safe_shutdown_loop/threading._DummyThread). Файл и код upload не входят в диф этой задачи (диф трогает толькоimport_export.pyиз бэкенда), само падение — про завершение потока, не про сериализацию маркеров/экспорт. Отношу к флаку окружения песочницы, а не к регрессии этой правки; целевой тест AC5 (test_issue_385_space_export_drops_badge_and_value_face_links_together) и весьtest_ha_import_export.py(запускался в составе полного прогона) прошли зелёным.
AC — прослеживаемость
| AC | Доказательство | Статус |
|---|---|---|
| AC1 | demo/smoke_value_face_source.mjs → sameBindingKeepsSource; регресс bindingResetToAuto |
прогнано лично, зелёное |
| AC2 | тот же смок → sameVirtualKeepsSource (плюс бейдж не проверяется отдельным полем смока, но реализация в (а) обрабатывает бейдж тем же early-return — проверено чтением) |
прогнано лично + чтением |
| AC3 | test/devices.test.mjs "#385(б) rewrite never plants…" |
прогнано в составе npm test |
| AC4 | test/process-gate.test.mjs "#385(в) the diff proof runs only for release-classified commits…" + мутант release-proof-computed-for-every-commit |
прогнано лично, мутант пойман 1/1 |
| AC5 | tests_backend/test_ha_import_export.py::test_issue_385_space_export_drops_badge_and_value_face_links_together |
прогнано лично, passed |
| AC6 | tsc/test/build/check-docs/bundle:budget — см. таблицу гейтов выше | прогнано лично, всё зелёное |
Вывод
Все шесть AC доказаны автотестами, которые лично прогнаны и (там, где предусмотрены мутанты) умеют падать. Единственная находка — Low, вне критического пути, снята записью с обоснованием. Реализация точно соответствует ТЗ rev2 по всем четырём пунктам, скоуп не расширен и не сужен.
Вердикт: зелёный.