16 KiB
CODE-REVIEW-529-r1
Issue: #529 — «Тупик "conflicting wall identifiers": план правится только после ручной чистки»
Материал: 85e5d61fb18360a3159000a5f971f62c239d11c7 (два коммита поверх b736878c: 49912ac9 — фикс, 85e5d61f — отпечаток скриншотов)
ТЗ: зелёное на r2 (docs/reviews/SPEC-REVIEW-529-r2.md), Medium r1 закрыт до кода
Заход: r1 · блокирующих циклов израсходовано 0/4
Скоуп
Диапазон origin/dev..HEAD: custom_components/houseplan/wall_segment_model.py,
custom_components/houseplan/validation.py, src/wall-segment-model.ts,
scripts/mutation-gate.mjs (3 новых мутанта), тесты
(tests_backend/test_wall_segment_model.py, tests_backend/test_ha_import_export.py,
test/wall-segment-model.test.mjs), три копии бандла, оба чейнджлога,
docs/CONFIG-COMPATIBILITY.md, отпечаток скриншотов документации.
Первый код-ревью-раунд по этой задаче — раздел «объём по дельте» (§2.10) не применяется, разбор полный.
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Дешёвые гейты на этом SHA | ссылка на Validate | success, https://github.com/Matysh/houseplan-card/actions/runs/34578867814 (headSha = материал ревью) |
| typecheck + build | npm run bundle:sync (= tsc --noEmit && rollup -c + синхронизация трёх копий) |
ok, git status после сборки — дерево чистое: локальная пересборка байт-в-байт совпала с закоммиченным дистом |
| unit | npm test |
2520 passed / 0 failed / 1 skip |
no-new-any |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
0 новых any (10 добавленных строк в 1 файле) |
check-docs |
node scripts/check-docs.mjs |
ok (7 файлов, 12 внешних ссылок) |
| выбор смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
1 прямое совпадение — demo/smoke_edit_walk.mjs (символ WallSegmentModelError) |
| смок | node demo/smoke_edit_walk.mjs |
ok, все 6 сценариев true |
| мутанты (патчи применимы) | node scripts/mutation-gate.mjs --check |
ok по всем, включая 3 новых |
| мутанты (реально красят свидетеля) | логи джобов Validate Мутанты по диффу (1/6, 5/6, 6/6) на этом же SHA |
все три новых мутанта: ok … тест покраснел, как обязан (см. таблицу AC7 ниже) |
| бэкенд | python -m pytest tests_backend -q |
не прогнан локально — в песочнице нет pytest/pytest_homeassistant_custom_component. Подтверждено джобой Validate «Бэкенд: pytest в Home Assistant: success» на этом самом SHA |
| golden / полные смоки / perf-смок | — | джобы skipped в Validate (не heavy-событие: обычный push, не PR/release/nightly) — ожидаемо: diff не меняет видимый рендер, AC не требует golden |
| model invariants на конкретном конфиге | — | не прогонялся: нет спорного экспортированного конфига пользователя; геометрические свойства (сохранение id сегмента, cm, детерминированное разрешение коллизии id) проверены AC2/AC3 напрямую, а npm test уже гоняет инварианты на всех моделях проекта |
check-inputs/классификация |
job Классификация изменённых файлов: success |
подтверждено CI |
AC — таблица доказательств
| AC | Чем доказан | Чем красится (мутация/негативная проба) |
|---|---|---|
| AC1 (backend) | test_empty_room_drafts_on_current_model_are_dropped_silently — прогон в CI Validate (success) |
косвенно AC7-мутант #1 (см. ниже) |
| AC2 (backend) | test_room_drafts_on_current_model_convert_exactly_like_the_first_migration — CI Validate |
AC7-мутант room-drafts-refuse-instead-of-heal |
| AC3 (unit) | test/wall-segment-model.test.mjs, оба новых #529-теста — прогнаны локально (npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test), 2/2 pass |
AC7-мутант room-drafts-mirror-still-throws |
| AC4 (backend) | test_outdated_client_is_recognised_by_the_carrier_not_by_the_model_number (циклы по moduleversion 9 и 10) + неизменённый test_stale_v9_room_draft_write_over_v10_is_rejected_before_schema — CI Validate |
AC7-мутант outdated-client-detected-by-model-number |
| AC5 (backend) | test_legacy_left_in_storage_is_not_treated_as_a_stale_writer (сторож не бросает) + правка теста инварианта схемы (пустой принимается, непустой отвергается) — CI Validate |
обратная проба читается в самом тесте (pytest.raises на непустом) |
| AC5а (unit) | #529: пустой room_drafts снимается молча в test/wall-segment-model.test.mjs — прогнан локально, pass; прочтением подтверждено, что commitWallSegmentModel вызывается в houseplan-editor-runtime.ts:2005,2030,2247,2405 и draft-live-commit.ts:79,168 перед отправкой config/set |
AC7-мутант room-drafts-mirror-still-throws |
| AC6 (backend) | test_export_is_not_locked_by_legacy_room_drafts — не прогнан локально (нет HA-окружения), подтверждён CI Validate («Бэкенд: pytest…: success» на этом SHA) + прочтением: _create_export (import_export.py:507-511) зовёт тот же commit_wall_segment_model, который после фикса не бросает |
— (не защитный AC в терминах «отказ», а «перестал отказывать»; негативная проба избыточна) |
| AC7 (мутанты) | реестр scripts/mutation-gate.mjs, 3 определения |
см. ниже — все три подтверждены логами CI на этом SHA |
| AC8 (ревью кода) | docs/CHANGELOG.md/docs/CHANGELOG.ru.md (в коммите User-Visible: yes), docs/CONFIG-COMPATIBILITY.md — читал текст, совпадает с фактическим кодом |
— |
AC7 — три мутанта, конкретные строки логов Validate (SHA 85e5d61f)
room-drafts-refuse-instead-of-heal(возвратraiseв питоновской миграции вместо тихого снятия/конвертации) → джоба «Мутанты по диффу (1/6)»,2026-09-11T08:26:31Z ok room-drafts-refuse-instead-of-heal: тест покраснел, как обязан.outdated-client-detected-by-model-number(возврат сверки поnew_model < 10вместоnot stored_drafts) → джоба «(5/6)»,2026-09-11T08:26:29Z ok outdated-client-detected-by-model-number: тест покраснел, как обязан.room-drafts-mirror-still-throws(TS-зеркало снова бросает вместо снятия ключа) → джоба «(6/6)»,2026-09-11T08:27:49Z ok room-drafts-mirror-still-throws: тест покраснел, как обязан.
--check (дешёвая часть, патчи применимы) прогнан локально и тоже зелёный; сам факт «тест умеет падать» проверен по реальным логам прогона мутаций в CI на материале ревью, а не только по заявлению автора.
Что проверено и корректно
- Порядок вызовов на пути
config/setиOptimize.validate_wall_model_transitionвызывается раньшеCONFIG_SCHEMAв обоих местах (websocket_api.py:1665-1666и:2033-2042) — риск, который сам автор попросил перепроверить («защита #478 не ослабла»), подтверждён прочтением кода. - Механизм самолечения хранилища (К4).
commitWallSegmentModel(клиентское зеркало) действительно вызывается перед формированием кандидатаconfig/setво всех точках структурной правки (houseplan-editor-runtime.ts:2005,2030,2247,2405,draft-live-commit.ts:79,168) — второй риск, названный автором, подтверждён прочтением. - Идентичность зеркал. Питоновская и TS-версии
_migrate_room_drafts_to_partitions/migrateRoomDraftsToPartitionsчитаются построчно идентично (конверсия драфта в partition, генерация детерминированного id при коллизии, снятие ключа). Паритетная фикстура#282не тронута и осталась зелёной (npm test). - Схемный инвариант (К4а).
_config_wall_segment_invariants(validation.py:1923) сменил"room_drafts" in spaceнаspace.get("room_drafts")— пустой список больше не триггеритvol.Invalid, непустой по-прежнему триггерит; тест на обе ветки существует и проверен построчно. - i18n. Новых ключей нет:
toast.wall_model_client_outdatedиbackup.error.wall_model_client_outdatedуже существуют во всех четырёх локалях (en/ru/de/fr) — заявление ТЗ подтверждено. - Changelog/докс. Оба чейнджлога в одном коммите с
User-Visible: yes;docs/CONFIG-COMPATIBILITY.mdпереписан согласованно с новым поведением (раздел «Ordinary wall chains — model v10»). - Трейлеры и провенанс.
Issue: #529на обоих коммитах;User-Visible: yesтолько там, где меняется поведение и есть оба чейнджлога;User-Visible: noна doc-коммите отпечатка скриншотов, который правит толькоscreenshots.json(imageSha256 не изменился ни в одном кадре — «identical»-путь, не полная пересъёмка). - Одно число — один источник: правка не вводит новых видимых пользователю величин (тексты тостов не менялись); неприменимо.
Находки
Нет. High: 0, Medium: 0.
Одно замечание отмечаю явно, чтобы не выглядело как незамеченное, но не поднимаю
до Low: питоновская _migrate_room_drafts_to_partitions использует
drafts = space.get("room_drafts") or [] — истинное не-списочное значение
(строка/число/словарь) не приводится к [] и не снимается молча, в отличие от
TS-зеркала (Array.isArray(...) ? ... : []), что формально не полностью
покрывает букву К1 («пустой список или не-список»). Прочтением подтверждено,
что это недостижимо на практике: единственная схема room_drafts
(validation.py:1743, vol.All([_LEGACY_ROOM_DRAFT_SCHEMA], ...)) требует
список уже при записи, и все вызовы commit_wall_segment_model (import_export.py,
junction_limits.py, серверный Optimize-путь websocket_api.py:2042-2050)
получают конфиг, уже прошедший CONFIG_SCHEMA на предыдущей записи или в этом
же вызове раньше. Мёртвый код, не находка.
Чего не проверял
python -m pytest tests_backend -qлокально — нет установленныхpytest/pytest_homeassistant_custom_componentв песочнице ревью; заменено ссылкой на зелёный прогон Validate на точном SHA материала плюс построчным чтением новых тестов и подтверждением по логам того же прогона, что три относящихся к этой задаче мутанта реально красят своих свидетелей.- Полный браузерный набор смоков,
golden:verify, perf-смок — не запускал: diff не меняет видимый рендер/геометрию отображения, AC их не требует; выборsmoke-select.mjsдал единственное прямое совпадение (smoke_edit_walk), оно прогнано и зелёное. npm run invariants -- --config <...>на конкретном экспортированном конфиге пользователя — такого конфига в материале ревью нет (issue не прикладывает файл, только синтетические конфиги автора и мои прогоны тестов); геометрические свойства миграции (сохранение id,cm, детерминированный id при коллизии) проверены напрямую через AC2/AC3 и общий прогонnpm test.- Ручного тестирования в UI не было (в процессе его нет в принципе) — вопрос «работает ли» закрыт цепочкой: клиентский юнит AC5а → серверные бэкенд-тесты AC1/AC2/AC4/AC5/AC6 → мутанты AC7, которые доказывают, что тесты способны упасть.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
Материал раунда
- Ветка:
issue/529-room-drafts-deadlock, коммит85e5d61fb183— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
51dee76b532b9bebeae46a0284f659aeb8be985cgit log --all --format='%H %T' | grep 51dee76b532b - Тело issue:
817b8acec5b1a72336cf4cb0ecc1c5d583d5a9d5b6fa55d2aea6be5e50190eb1 - Вердикт конвейера:
green· High 0