Files
houseplan-card/docs/reviews/CODE-REVIEW-377-r1.md
T
2026-08-29 21:41:15 +03:00

200 lines
17 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-377-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/377
- Заход: r1 (первый прогон код-ревью; предыдущая попытка `S7` вернулась в `S6`
из-за конфликта ребейза до того, как код кто-либо читал — цикл не
расходовался, см. комментарий владельца от 18:17)
- Ветка: `issue/377-decor-default-persist`, ребейзнута на `origin/dev` с уже
смерженным #375
- SHA материала ревью: `2edaf1adac60f9939b836da87f5f1f549dab2548`
(`git rev-parse HEAD` непосредственно перед подведением итогов)
- Диапазон: `git diff origin/dev...HEAD` — 33 файла, +789/-468
## Скоуп
Персист серверного ключа `settings.decor_default_style` (цвет/стиль декора
«по умолчанию» из тулбара Background-редактора, #360): переживает
перезагрузку, общий для всех редакторов плана. Три поверхности: схема
(`validation.py`), инициализация (`houseplan-card.ts`), запись
(`houseplan-editor-runtime.ts`) — ровно то, что называла аналитика и ТЗ
(SPEC-REVIEW-377-r2, зелёный).
## Как проверялось
Полный разбор (первый заход код-ревью для этой задачи; §2.10 неприменим).
Читал построчно: `src/editors/decor/geometry.ts` (новые
`decorStyleFromSettings`/`decorStyleToSettings`), `src/houseplan-card.ts`
(`_seedDecorStyle` и оба места вызова), `src/houseplan-editor-runtime.ts`
(`_updateDecorStyle`/`_persistDecorStyle`, все шесть UI-точек изменения
стиля), `custom_components/houseplan/validation.py` (схема),
`custom_components/houseplan/import_export.py` (путь `settings` при полном
импорте — подтверждает AC6 сверх юнита), тесты (`test/decor-style-persist.test.mjs`,
`tests_backend/test_validation.py`, `demo/smoke_decor_default_persist.mjs`),
изменения в `scripts/mutation-gate.mjs`, `test/color-picker.test.mjs`, оба
changelog и оба USER-GUIDE.
### Гейты — прогнал сам (зелёного Validate на этом SHA нет)
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто |
| Юниты | `npm test` | 1560 pass / 0 fail / 1 skipped (совпадает с заявкой автора) |
| Сборка | `npm run build` | ok, 14.5s |
| Синхронизация бандлов | `npm run bundle:sync` | ok; `git status --porcelain` после — пусто (три копии дерева байт-в-байт совпадают с закоммиченными) |
| Бюджет | `npm run bundle:budget` | initial View 276 221 Б / 300 000 (запас 23 779 Б) — проходит; отличие от заявленных автором 276 059 Б в пределах шума нединамического build (не влияет на вердикт) |
| Доки | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — diff трогает `src/**`, гейт обязателен и зелёный |
| Backend (чистое подмножество) | `python -m pytest tests_backend -q --ignore=tests_backend/test_coordinate_canonicalization.py` (после `pip install pytest voluptuous`, в базовом окружении ревьюера их не было) | 235 passed, 1 skipped — совпадает с заявкой автора байт-в-байт. `test_coordinate_canonicalization.py` падает на `ModuleNotFoundError: homeassistant` при коллекции (не `test_ha_*.py`, поэтому `conftest.py` его не глушит) — это существующий пробел окружения, не связан с #377, полный HA-harness — дело CI |
| Единственный источник числа | `node --test test/single-source-numbers.test.mjs` | 3/3 pass; смысловая часть — см. ниже |
| Мутанты (только новые, точечно `--id=`) | `node scripts/mutation-gate.mjs --id=decor-default-style-seed-cut` и `--id=decor-default-style-debounce-cut` | оба «поймано 1 из 1» — тест умеет падать |
Полный `mutation-gate.mjs` без `--id=` не гонял: это полная матрица всех
мутантов проекта (несоразмерно точечной правке), к тому же в этом окружении
падает на несвязанном backend-guard'е (тот же отсутствующий `homeassistant`).
Точечный `--id=` — правильный инструмент здесь.
### Смоки — выбор через `scripts/smoke-select.mjs --base origin/dev --head HEAD`
Инструмент дал 7 «прямых совпадений» и 13 «слабых связей» (все — по одному
имени `_saveConfig`, geometry/wall-thickness в диффе не тронуты).
Прогнал все 7 прямых совпадений — все OK:
`smoke_decor_default_persist` (новый, AC4/AC5), `smoke_bg_color`,
`smoke_color_picker`, `smoke_decor`, `smoke_dialog_zombie`, `smoke_furniture`,
`smoke_general_settings`.
Из слабых связей прогнал `smoke_config_writer` (тот же, что заявлен автором) —
OK. Остальные 12 слабых связей не гонял: диф не трогает геометрию/стены/
толщину, разделяют с диффом только имя `_saveConfig`, чей вызывающий код в
затронутых местах не менялся по поведению (только маршрутизация через
`_updateDecorStyle`). `npm run invariants` не требовался — diff не трогает
рёбра комнат, `layout`, `marker.space`, `open_spans` или записи толщины.
`npm run golden:verify` не гонял: ТЗ прямо утверждает «видимых изменений
нет», и это подтверждается кодом — `_decorStyle` управляет только дефолтом
для НОВЫХ объектов декора/мебели (`_renderDecorLayer` draft-превью,
`_renderFurniturePlacementPreview`), View эти пути не рендерит, а
`imageSha256` всех эталонных скриншотов, кроме одного, не изменился;
единственное расхождение (`06-device-display-preview.png`, 326512→326513
байт) — разница в 1 байт кодирования PNG, не связанная с decor-путём
скриншота и не относящаяся к этой правке по содержанию сцены; `check-docs`
(сверяет `sourceFingerprint`) зелёный.
## Находки
Нет ни одной High или Medium-находки.
**Low (снята без правки).** `_updateDecorStyle` заводит собственный
`window.setTimeout(…, 1000)` (`houseplan-editor-runtime.ts:9403-9411`) и не
чистит его в `disconnectedCallback` карты, в отличие от штатного
`_saveConfigDebounced.flush()` (`houseplan-card.ts:2638`, «never leave an edit
unsent on teardown»). Разобрано по коду (не исполнением): в реальности потерю
данных это создаёт только при закрытии/перезагрузке вкладки строго внутри
окна 1 с после последнего изменения цвета — при обычном пересоздании дерева
Lovelace (тот случай, для которого существует `disconnectedCallback`) таймер
переживает как обычный JS-таймер и запись всё равно долетает, просто позже.
Цена ошибки — вернуться к прежде выбранному цвету, ценность правки в рамках
этой задачи не оправдана: снимаю с записью, правки не требую.
## Проверка AC
- **AC1** (pytest, валидация): доказано исполнением —
`tests_backend/test_validation.py::test_decor_default_style_setting`,
10 мусорных вариантов + `"solid"` вместо объекта, все `pytest.raises`.
Прогнал сам, 235/0 в подмножестве, включая этот тест.
- **AC2** (юнит фронта, мердж): доказано исполнением —
`test/decor-style-persist.test.mjs` («all six fields cross the boundary»,
частичный ключ, мусорные поля). Мутант `decor-default-style-seed-cut`
ловит вырезание вызова — 1/1.
- **AC3** (юнит фронта, легаси): доказано исполнением — тот же файл,
`undefined/null/'x'/7` → `DEFAULT_DECOR_STYLE`.
- **AC4** (смок, одна запись после дебаунса + сброс к дефолту без ключа):
доказано исполнением — `demo/smoke_decor_default_persist.mjs`, прогнал сам,
OK. Мутант `decor-default-style-debounce-cut` (дебаунс → 0 мс) ловится —
1/1, подтверждает, что смок действительно проверяет окно, а не просто
наличие записи.
- **AC5** (смок, посев новой карты из ключа): доказано исполнением — тот же
смок, `seeded` (частичный ключ наследует остальные поля дефолта).
- **AC6** (round-trip импорт/экспорт): доказано исполнением на уровне схемы
(`test_decor_default_style_setting`, byte-identical) плюс разобрано
чтением, не исполнением — `import_export.py:1661-1669`: для non-same-source
импорта из `settings` вычищаются только `known_devices`/`new_device_ids`,
`decor_default_style` не в списке и проходит насквозь; `config = CONFIG_SCHEMA(config)`
применяет ту же схему, что и юнит. Отдельного юнита самого
`import_export.py` на этот ключ нет — при полном треке для «переносится
штатно, без спецобработки» это достаточное доказательство: код, который мог
бы его выбросить, прочитан и не делает этого.
- **AC7** (гейт + недостижимость в холодном View): гейты выше зелёные.
Недостижимость в View разобрана чтением: `_updateDecorStyle`/
`_persistDecorStyle` существуют только в `HouseplanEditorRuntime`
(`houseplan-editor-runtime.ts`), которая грузится лениво только при входе в
редактор; во всём `houseplan-card.ts` `_decorStyle` только читается
(превью черновика/мебели), никогда не пишется вне рантайма. Холодный View
этот модуль не подгружает — записи там физически нет.
## Что проверено и корректно
- Схема `validation.py` соответствует ТЗ п. «Модель данных» дословно:
диапазоны, `_COLOR`, все поля опциональны.
- Единственная точка конверсии snake_case↔camelCase
(`decorStyleFromSettings`/`decorStyleToSettings`) — ровно то, что требовал
ТЗ п.7 после SPEC-REVIEW-377-r1 M1; сопоставил маппинг 1:1 с текстом ТЗ.
`decorStyleToSettings` возвращает `null` при точном совпадении с дефолтом
(сравнение по значению всех 6 полей) — паттерн «дефолт = отсутствие ключа»
выдержан.
- Посев (`_seedDecorStyle`) срабатывает ровно один раз на инстанс
(`_decorStyleSeeded`) и из обоих источников первого конфига — тёплого
кэша (`localStorage`, `houseplan-card.ts:3062-3066`) и первого серверного
ответа (`_adoptStructuralResponses`, `houseplan-card.ts:4045-4046`);
последующие обновления конфига не переоткрывают уже идущую сессию
редактирования — сознательное и разумное техническое решение (в тексте
явно откомментировано), продуктовым контрактом ТЗ не описано и не
противоречит ему (сценарий ТЗ — «назавтра» и «второй редактор на другом
устройстве», т.е. холодный старт, а не горячее обновление во время сессии).
- Запись идёт исключительно штатным сериализованным путём (`_saveConfig` →
`_saveConfigDebounced` → `_writeConfig` → `_sendConfigCandidate` с
`expected_rev`) — новый канал не создан, конфликт/тост наследуются
бесплатно.
- Все шесть UI-точек изменения стиля (`_decorSaveShape`, тулбар,
контекстный трей: цвет/непрозрачность, ширина, fill-чекбокс, fill-цвет,
ещё раз в `_renderDecorBar`) переведены на `_updateDecorStyle` — ни одна не
забыта (сверил построчно с прежними местами прямого присваивания
`host._decorStyle =` в диффе).
- Трейлеры (`Issue: #377`, `User-Visible: yes/no`) на месте во всех трёх
коммитах; оба changelog и оба USER-GUIDE — в том же коммите, что
поведение (`ec76948b`), не отдельным «допишу потом».
- Мутант-регистрация (`scripts/mutation-gate.mjs`) описывает ровно тот
дефект, который она ловит («seed-cut» — тихая деградация посева, приводит
к молчаливому нерабочему AC2/AC5; «debounce-cut» — спам записей),
соответствует формулировке в ТЗ «План автотестов».
- «Один источник числа»: новый ключ не дублирует уже показываемое где-то
значение (это предпочтение по умолчанию для будущих объектов, а не
производная величина, показанная дважды) — `test/single-source-numbers.test.mjs`
зелёный, смысловых дублей не нашёл.
## Чего не проверял
- Полный `npm run golden:verify` и полный `demo/smoke_*.mjs` (все ~204) — не
оправдано объёмом правки; обоснование выбора — выше.
- Полный HA backend harness (`test_ha_*.py`, `test_coordinate_canonicalization.py`)
— недоступен в этом окружении (нет `homeassistant`), это гейт CI/бета, не
код-ревью; расхождение не связано с #377.
- Живое ручное тестирование в браузере (ручного цикла в процессе нет) —
замещено чтением кода и прогоном автотестов/смоков выше.
- Гонка двух вкладок (штатный conflict #340) отдельно не воспроизводил —
путь записи переиспользует существующий механизм, для которого этот
сценарий уже покрыт в других смоках (`smoke_save_race.mjs`, не гонял —
слабая связь, поведение конфликта в этой задаче не менялось).
- `import_export.py` полный интеграционный юнит именно на
`decor_default_style` (только чтение кода + существующий байт-в-байт юнит
схемы, см. AC6 выше).
## Вердикт
Все 7 AC доказаны (автотестом там, где это дешевле и надёжнее; чтением кода —
там, где ТЗ само на это полагалось, с явной пометкой). High/Medium-находок
нет, одна Low снята с записью. Гейты, соразмерные объёму задачи, зелёные.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
Готово к очереди на пре-релиз.