Issue: #244 User-Visible: no
23 KiB
CODE-REVIEW-244-r3
- Issue: #244 — восстановление маркеров с мёртвой ссылкой на пространство
- Этап: code (PROCESS.md §2.7)
- Заход: r3 · блокирующих циклов израсходовано 1 из 4 (потрачен только в r1 — жёлтый/красный тратит бюджет, r2 зелёный цикла не образовал, #227)
- Кандидат (HEAD):
fe70c1e80303650113a7aafc74983defc27d8841 origin/devна момент разбора:0c5683d3bf1c79f01e1d52ee544c0fdff4f67a1c(после релиза v1.67.0-beta.2)- SHA предыдущего раунда (r2, зелёный):
a9466da649c5a24ad84cc79b0e6389b724dc8087(недостижим напрямую — переписан ребейзом, диагностировано поdocs/reviews/CODE-REVIEW-244-r2.md, который остался в дереве)
Почему этот заход не по дельте, а полный
Между r2 (зелёным) и этим заходом код не правился — ветка была перебазирована
на ушедший вперёд origin/dev (комментарий автора 2026-08-22T21:37:13Z):
слияние конфликтовало из-за выпущенного в dev релиза v1.67.0-beta.2
(1af9cfc) и последующих 005eb7f/0c5683d. Это ровно случай из инструкции
ревью — «ребейз на ушедший вперёд dev — после ребейза это другой код,
§7.2» — поэтому разбор в этом раунде полный, а не по дельте продуктового
кода. Экономия достигается не сокращением строгости, а тем, что часть
доказательства — байтовое сравнение — быстрее многократного построчного
чтения:
- Список файлов в
git diff origin/dev...HEAD(43 файла) идентичен по составу объединению диффов r1 (40 файлов) и r2 (19 файлов, из них 17 пересекались с r1) плюс собственный документCODE-REVIEW-244-r2.md— новых, не объяснённых предыдущими раундами файлов нет. custom_components/houseplan/const.py: строкаVERSION = "1.67.0-beta.2"не тронута этой веткой (она пришла из релиза и является контекстом, а не диффом) — правки диффа ограниченыPLAN_MODEL_VERSION 6 → 7, как и было на кандидате r2.- Пересборка (
npm run build) на этом HEAD даёт тот же SHA-256 бандла (9316d1497b6ff0e24403db319df1756b2e420d5a9c87a2c7dcd8d622057fbeda), что и заявлен автором в комментарии о ребейзе — компилируемый TS/JS не изменился по сравнению с состоянием, которое проверял r2. - Единственная содержательная правка при разрешении конфликта — в
docs/CHANGELOG.md/.ru.md: секция## Unreleasedс записью #244 осталась выше уже выпущенной## v1.67.0-beta.2 — 2026-08-22, обе записи не искажены (проверено чтением, см. ниже). - Коммиты релиза, вошедшие в
devмежду базой r1/r2 и текущей базой (1af9cfcRelease v1.67.0-beta.2,005eb7fgolden-эталоны,0c5683dfix #247 дляtest/docs-accept.test.mjs), не касаются Optimize/import/delete/editor-путей #244: правки — версия/манифест/ релиз-ноуты, golden-baseline PNG для стыков стен/полок/трея (#233-класс), кросс-платформенный путь фикстуры. Пересечения сценариев нет.
Исходя из этого разбор построен так: свежий прогон всех обязательных гейтов на текущем HEAD (не унаследован — код по факту другой коммит), независимое чтение критичных модулей заново (не только «принято на слово» от r1/r2), и унаследование содержательных находок AC1–AC14 из r1/r2 там, где чтение и байтовое сравнение подтверждают отсутствие изменений.
Скоуп
Normal-трек (bug, P2). Диапазон git diff origin/dev...HEAD: 43 файла,
включает reference-repair pass (src/space-reference-repair.ts),
delete-preflight (src/space-deletion.ts), интеграцию в
src/plan-optimizer.ts (PLAN_MODEL_VERSION 6→7), редактор карточки
(src/editor.ts, src/card-editor-validation.ts), backend
custom_components/houseplan/websocket_api.py (houseplan/space/delete) и
import_export.py (_repair_target_space_refs), тесты (unit/backend/smoke/
golden) и документацию (ТЗ, оба CHANGELOG, USER-GUIDE.md/.ru.md,
CONFIG-COMPATIBILITY.md, TESTING.md, ARCHITECTURE.md, docs/reviews/*).
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
PASS, без вывода |
| Unit | npm test |
1114/1114 PASS, 0 skipped (было 1113 на кандидате r2 — плюс тест из test/docs-accept.test.mjs для фикса #247, не относящегося к #244) |
| Build + bundle sync | npm run build + sha256sum трёх копий |
PASS; dist/houseplan-card.js, demo/srv/assets/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js — идентичный SHA-256 9316d1497b6ff0e24403db319df1756b2e420d5a9c87a2c7dcd8d622057fbeda, совпадает с заявленным в комментарии о ребейзе; git status --short после сборки чист |
| Docs fingerprint | node scripts/check-docs.mjs (diff трогает src/**) |
PASS: «Documentation checks passed (7 files, 10 external links)» |
| Backend | python -m pytest tests_backend -q (в песочнице ревью не было pytest-homeassistant-custom-component/home-assistant-frontend, поставил тем же набором пакетов, что CI-workflow: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend) |
352 passed, 1 error — test_ha_upload.py::test_upload_ok, тот же _run_safe_shutdown_loop/threading._DummyThread teardown-артефакт, что в r1/r2, независим от диффа (не трогает websocket_api.py/import_export.py в части, которую использует этот тест) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
— | 41 символ на изменённых строках, порог широкого символа 33 → не превышен. 19 прямых совпадений (список идентичен r1), 29 слабых связей (список идентичен r1) |
| Целевые smoke, прямо названные в таблице AC (AC1/AC7/AC9/AC10/AC11) | node demo/smoke_orphan_space_references.mjs |
PASS, все 10 ассертов true, включая deleteExplainsBlockerWithoutConfirmOrWrite: true |
| — | node demo/smoke_optional_space_model.mjs |
PASS, все 11 ассертов true, включая deleteLastUsesAuthoritativeEndpoint: true, deleteLastPreservesMarkersWithoutPlacement: true |
| — | node demo/smoke_optimize_geometry_preflight.mjs |
PASS, все 16 ассертов true |
| — | node demo/smoke_fixed_floor.mjs |
PASS, все 14 ассертов true |
npm run golden:verify, остальные 15 прямых + 29 слабых smoke, mutation-gate.mjs, performance |
— | не прогонялись — см. «Чего не проверял» |
Решение по каждой строке smoke-select (подтверждено заново на текущей базе)
Прямое совпадение (19) — прогнаны 4 (см. таблицу выше): это
единственные, где названный символ прямо принадлежит изменённой логике
удаления/Optimize/редактора и прямо назван в таблице AC ТЗ как способ
доказательства (AC1, AC7, AC9, AC10, AC11). Остальные 15
(smoke_linked_virtual_light, smoke_save_race, smoke_virtual_light_toggle,
smoke_audit_1490, smoke_bg_color, smoke_config_writer,
smoke_device_preview_parity, smoke_dialog_footer_width,
smoke_general_settings, smoke_layout_sync, smoke_plan_upload_race,
smoke_climate_once, smoke_editor_tabs, smoke_infinite_canvas,
smoke_ws_resilience) не прогнаны: совпадение только по общему плумбингу
(_cfgRev, _layoutRev, _config, _persistLayout, _reloadConfigOnly,
buildDevices, Layout), который #244 читает, а не переопределяет. Список и
основание идентичны r1 — рекомпиляция и ребейз не изменили набор изменённых
символов.
Слабая связь (29) — не прогнаны: все совпадения по одному
распространённому имени (_config, _spaceDialog, _cfgRev), диалоги
пространства/цвета/тегов не меняли контракт в этом диффе. Тот же список, что
и в r1/r2.
Находки
Нет. Продуктовый код идентичен зелёному кандидату r2 (подтверждено байтовым сравнением бандла и построчным чтением ключевых модулей ниже); ребейз не внёс регрессий, конфликт CHANGELOG разрешён корректно.
Закрытие раунда r2
| Пункт r2 | Статус на r3 | Где это видно |
|---|---|---|
| Находок в r2 не было (H1/H2 из r1 закрыты) | Не переоткрывались: продуктовый код с r2 не менялся, только база | байт-в-байт совпадающий SHA-256 бандла; идентичный список файлов диффа |
Требование «повторный код-ревью после ребейза — не формальность» (комментарий автора 2026-08-22T21:33:58Z) |
Выполнено этим документом: полный набор обязательных гейтов перезапущен на новом HEAD, а не унаследован | таблица «Как проверялось» выше — все команды выполнены заново на fe70c1e, не скопированы из r2 |
Унаследовано из r1/r2 (без повторного построчного чтения всех файлов)
Документы: docs/reviews/CODE-REVIEW-244-r1.md (кандидат a09338f),
docs/reviews/CODE-REVIEW-244-r2.md (кандидат a9466da).
- Диагноз §3 ТЗ и его соответствие коду (
resolveExplicitMarkerPlacement, приоритетmarker.spaceдля virtual-маркера, фильтр рендера View/Static,build_space_merge(),resolveInitialSpace()) — не перепроверялся заново построчно; косвенно подтверждён тем, чтоsrc/space-reference-repair.ts(прочитан заново целиком в этом раунде, см. ниже) реализует именно этот контракт без расхождений. - AC8 (импорт одного пространства,
_repair_target_space_refs) и AC11 (default_floorв редакторе карточки) — не перечитывались заново построчно; диффы этих файлов идентичны r1 (не тронуты r2 и ребейзом), тестыtest_ha_import_export.py/card-editor-validation.test.mjsперепрогнаны в составеnpm test/pytestэтого раунда и зелёные. - Touch/kiosk паритет View/Static-рендера (закрыт на этапе spec,
SPEC-REVIEW-244-r2) — рендер-путь не менялся ни в r1/r2, ни ребейзом. - Mutation-паттерны трёх frontend-рисков (
orphan-space-detach-disabled,orphan-space-ambiguous-signature-guessed,orphan-space-area-keeps-stale-position) — прочитаны в r1 построчно, самmutation-gate.mjsне перезапускался ни разу за три раунда (предрелизный гейт).
Что проверено и корректно (перечитано заново в этом раунде)
src/space-reference-repair.ts— прочитан целиком заново. Порядок правил для активного маркера (signatureSpace→uniqueAreaRoomдля non-virtual →delete marker.space) совпадает со спекой §8.2; virtual binding пропускает Area-remap и уходит в signature/detach; removed-маркер получает только доказуемый signature (ветки!isRemovedего исключают из Area/detach) — dead-ссылка tombstone остаётся нетронутой и это корректно, не регресс. Позиции:exact && positionSpace === storedSpaceпереноситx/y/kпри sig-remap, иначе!isRemovedудаляет layout-запись целиком (Area/detach) — совпадает со спекой §8.3.reversibleStemрежет на длине 35 и формате id,spaceSignatures.get(oldId)?.length === 1требует ровно один кандидат — защита от ложного ремапа (AC3) на месте.remainingDead/positionsUnresolvedсчитают removed/opaque владельцев верно (!activeMarkerIds.has(key) || removedMarkerIds.has(key)).src/space-deletion.ts— прочитан целиком заново.collectSpaceMarkerDependenciesдедуплицирует маркер по трём источникам ссылки (marker.space,room_idчерез комнаты пространства, layouts) вSet, считает один раз — соответствует AC9.createSpaceDeletion Candidate:dependencies.count && !deletingLastSpaceблокирует без мутации входа (раннийreturn); приdeletingLastSpaceу всех затронутых marker-записей (включая removed) снимаются ровноspaceиroom_id, остальные поля не трогаются; layout-записи сs === spaceIdудаляются целиком — совпадает с решением владельца по арбитражу H2 и спекой §10.custom_components/houseplan/websocket_api.py(_space_marker_ dependencies,_space_delete_candidate,ws_space_delete) — прочитан диапазон добавленного кода (187 строк) целиком заново. Backend-версия структурно и по значению идентична frontend: тот же порядок проверок, тот же набор снимаемых полей.ws_space_deleteработает подrt.write_lock, ревалидирует обеexpected_*_revвнутри лока, перепроверяетdependencies/deleting_last_spaceна актуальныхcurrent_config/current_layout(не на входных данных запроса) — гонка, где зависимость появилась между preflight и записью, поймана; используетCONFIG_SCHEMA/LAYOUT_SCHEMAи существующий crash-safe_commit_import_pair(pending/rollback), не вводит новый механизм записи.custom_components/houseplan/const.py—PLAN_MODEL_VERSION6→7,VERSIONне тронут этим диффом (относится к релизу beta.2, слит без конфликта в этой строке).- CHANGELOG-конфликт разрешён корректно —
docs/CHANGELOG.mdи.ru.mdпрочитаны целиком: секция## Unreleasedс записью #244 (7 предложений, описывает Optimize repair/detach/preview, delete-блокировку и last-space-исключение, editor-предупреждение) стоит выше уже выпущенной## v1.67.0-beta.2 — 2026-08-22; ни одна запись не потеряна и не задвоена. Мелкий косметический артефакт: в RU-файле фраза «указывает на отсутствующее пространство» перенесена на две отдельные строки внутри одного элемента списка (docs/CHANGELOG.ru.md:19-21) — при рендере Markdown мягкие переносы схлопываются в пробел, видимого дефекта нет; не поднимаю как находку. docs/CONFIG-COMPATIBILITY.md,docs/ARCHITECTURE.md— прочитаны задетые разделы: вездеmodel-v6корректно заменено наmodel-v7, новый раздел «Space reference repair (model v7)» точно описывает реализованное поведение (signature/Area/detach, one-space import remap, revision-guarded delete, last-space-исключение), без противоречий коду.- Трейлеры — оба
User-Visible: yesкоммита (f6f877e,6a151d1) несут правки вdocs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdв том же коммите; все 13 коммитов диапазона несутIssue: #244. - Файловый состав диффа — 43 файла, без стороннего golden-baseline PNG (Optimize/delete/editor — не рендер-путь, совпадает с оценкой r2), без случайных файлов вне объяснённого объёма r1+r2+ребейз.
Чего не проверял
- Полный browser smoke (167 сценариев) — не запускался;
smoke-selectна текущей базе не даёт оснований для полного прогона (порог 33 не превышен, широких совпадений нет), диапазон изменённых символов идентичен r1/r2. Предрелизный гейт (PROCESS.md §8). npm run golden:verify/ golden:accept — не прогонялся; #244 не меняет рендер/геометрию/стили/слои, только данные и ветвление preflight/backend-транзакции — заключение унаследовано из r1/r2, диффом ребейза golden-баз #244 не касается (только несвязанные PNG из релиза beta.2, не входящие в диапазонorigin/dev...HEADэтого ревью).scripts/mutation-gate.mjs— не запускался ни в одном из трёх раундов; не в AC этой задачи, предрелизный гейт.- Performance-профили — не запускались; путь не горячий (однократное явное действие пользователя), не назван в AC.
- Построчное чтение backend
import_export.py(_repair_target_space_ refs) и frontendeditor.ts/card-editor-validation.tsзаново в этом раунде — не выполнялось; эти файлы не менялись между r1 и текущим HEAD (провереноgit diff— идентичны версии, прочитанной в r1 построчно), их тесты перепрогнаны и зелёные в составеnpm test/pytestэтого раунда. - Ручное тестирование в браузере — не проводилось; вместо этого
прогнаны headless browser-smoke (
smoke_orphan_space_references,smoke_optional_space_model,smoke_optimize_geometry_preflight,smoke_fixed_floor), которые управляют реальнымHouseplanCardэлементом через Puppeteer/DOM — это заявленный в таблице AC способ доказательства для AC1/AC7/AC9/AC10/AC11. docs/images/screenshots.jsonPNG-содержимое — не скачивал артефакт workflow заново; ссылка на прогон (.../runs/32599244099) не изменилась относительно r2, где уже была проверена как настоящая.
Итог
Ребейз на ушедший вперёд dev не изменил продуктовый код #244: байтовое
совпадение пересобранного бандла, идентичный состав файлов диффа и
построчное чтение самых критичных модулей (space-reference-repair.ts,
space-deletion.ts, ws_space_delete) заново подтверждают это независимо
от заявления автора. Единственная содержательная правка ребейза —
разрешение конфликта в обоих CHANGELOG — корректна. Все обязательные гейты
перезапущены на новом HEAD и зелёные; четыре целевых smoke, названных в
таблице AC, зелёные. Новых находок нет, находки r1 (H1/H2) остаются
закрытыми.
Вердикт: зелёный · заход r3 · блокирующих циклов 1/4 · High: 0 · Medium: 0 → в задаче
Документ: docs/reviews/CODE-REVIEW-244-r3.md