Files
houseplan-card/docs/reviews/CODE-REVIEW-385-r1.md
T
2026-08-30 08:24:40 +00:00

176 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
- `353724b2` fix: audit-lows batch from the v1.69.0 audit (#385) — User-Visible: yes, Issue: #385
- `564bf419` build: 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_badge`
fallback → ключ не создаётся. Тест `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 по всем четырём пунктам, скоуп не расширен и не сужен.
Вердикт: **зелёный**.