Files
houseplan-card/docs/reviews/CODE-REVIEW-456-r2.md
T
claude[bot] 6af6d7b388
Проверка (CI) / Классификация изменённых файлов (push) Successful in 23s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 1m5s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 48s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 19s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 24s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 7m4s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 10m10s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
docs: review document for #456
Issue: #456
User-Visible: no
2026-09-05 06:37:26 +00:00

23 KiB
Raw Blame History

CODE-REVIEW-456-r2

  • Issue: #456 — копирование пространства без комнат и устройств
  • Ветка: issue/456-copy-space
  • SHA материала: 7dd5b4798a056b7f31dbb48ff00e833d90df5b9c (сверено git rev-parse HEAD непосредственно перед выводом вердикта)
  • Заход: r2 · блокирующих циклов израсходовано до этого разбора: 1/4 (зелёный вердикт этого захода бюджет не тратит, #227)
  • ТЗ: docs/specs/456-copy-space.md, SPEC-REVIEW зелёный (docs/reviews/SPEC-REVIEW-456-r1.md)
  • Предыдущий раунд: docs/reviews/CODE-REVIEW-456-r1.md, вердикт жёлтый, High: 0, Medium: 3 (в скоупе), SHA материала 2c355ee9ec97ce65fd6b9525f3f82cca3668c8e8

0. Материал r1 и почему разбор — по дельте, а не заново

SHA 2c355ee9 мёртв (git cat-file -t — «could not get object info»): это ожидаемо, автор в хендоффе прямо пишет «Ветка приведена к актуальному dev» — между r1 и r2 сделан ребейз. Дерево материала r1 из блока «Материал раунда» (3da0804bd0118dde2f3ccfcc41e253a73ce8ab66) тоже не резолвится ни в одном объекте репозитория — ожидаемо, ребейз меняет полное дерево коммита, даже если патч коммита не менялся.

Проверил, не «другой ли это код» (§7.2/§2.10 требуют полного разбора при таком ребейзе), контентным сравнением, а не на слово автора:

  • origin/dev на момент r1 был 6ef0804b (указано в CODE-REVIEW-456-r1.md), сейчас origin/dev = de30066b.
  • git log --oneline 6ef0804b..de30066b — ровно один новый коммит: de30066b test(gates): мутанты на восемь модулей горячего пути из #451, класс B (scripts/mutation-gate.mjs, test/mutation-gate.test.mjs, только добавления — 145 строк), не задевает ни один файл из src/space-copy*/space-dialog/houseplan-*-runtime.
  • Коммит feat: copy spaces without room bindings (b286d7ba, текущий эквивалент бывшего 2c355ee9) при диффе против нового de30066b показывает scripts/mutation-gate.mjs как +115 строк (11 новых мутантов #456, дописанных в конец существующего массива) — то есть содержимое патча идентично тому, что описано и построчно проверено в CODE-REVIEW-456-r1.md §4; ребейз сдвинул только базу, не тело коммита. Конфликтов не было (иначе диапазон дат/структура коммитов были бы другими).

Вывод: ребейз реальный, но нелокальным (§2.10 «другая подсистема / смена контракта») не является — задета только инфраструктура мутационного гейта другой задачи (#451), без пересечения с кодом #456. Разбираю по дельте: r1-материал восстановлен по коммиту b286d7ba (патч-эквивалент бывшего 2c355ee9, тот же список файлов, тот же diffstat, что зафиксирован в CODE-REVIEW-456-r1.md §1/§4), дельта этого раунда — три коммита поверх него плюс коммит документа ревью:

b286d7ba feat: copy spaces without room bindings          (= материал r1, патч не менялся)
6dd9fb03 docs: refresh screenshot source fingerprint       (M1)
2c787913 test: account for space copy action               (M3, часть — footer width)
51082024 docs: review document for #456                    (публикация CODE-REVIEW-456-r1.md)
7dd5b479 test: cover the space copy workflow                (M2 golden accept + M3 новый смок)

Ни один файл src/**/custom_components/**/*.py в дельте не тронут — вся дельта класса B/C/D (демо-смоки, golden-эталон, документация). Значит AC1–AC13 (функциональность фичи) дельта не задевает: доказательства r1 (11 реальных прогонов мутантов + живой браузерный E2E) наследуются без повторного прогона. Повторной проверке подлежат ровно три находки r1.

1. Скоуп (не изменился с r1)

Кнопка «Копировать» в footer настроек пространства (edit-only, вне dialog-action-danger), диалог имени, предварительный whole-plan Optimize с warning-подтверждением, модуль src/space-copy.ts конвертирует wall_segments[]+partitions[] источника в самостоятельные перегородки копии с перевязкой openings[] и очисткой contact/lock. Комнаты/устройства/markers/layout не переносятся.

2. Как проверялось

2.1 Дешёвые гейты — прогнаны заново (код изменился, стоят минуты)

Гейт Команда Результат
Typecheck npx tsc --noEmit зелёный, без вывода
Unit/integration npm test 1975 tests, 1974 passed, 0 failed, 1 skipped — совпадает с хендоффом автора
Build + bundle parity npm run build && npm run bundle:sync && cmp dist/houseplan-card.js custom_components/.../houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js зелёный, все три копии синхронны
node scripts/no-new-any.mjs --base origin/dev --head HEAD — зелёный: «Новых any нет» (600 добавленных строк в 6 файлах)
node scripts/process-gate.mjs — «гейт пройден, предупреждений 0» (диапазон origin/dev..HEAD, 7 коммитов)

2.2 Гейты, закрывающие находки r1 — прогнаны целиком (не только для проверки на слово автора)

Гейт Команда Результат
node scripts/check-docs.mjs (M1) — зелёный: «Documentation checks passed (7 files, 12 external links)»
npm run golden:verify (M2, полный матрикс — --scenario= на verify запрещён самим скриптом, «must run the complete matrix») — 153/153 passed, включая space-room-color-popover-desktop-ru
node demo/smoke_space_copy.mjs (M3) — зелёный, все 7 проверок true (см. §3 разбор содержимого)
node demo/smoke_dialog_footer_width.mjs (правки M3 в этом файле) — зелёный, space/space_de теперь buttons: 4 (было 3) на desktop и narrow
node scripts/mutation-gate.mjs --check — зелёный, реестр валиден, все 11 мутантов #456 на месте (space-copy-title-always-reuses-two … space-copy-does-not-select-result)

2.3 Не прогонялось повторно — обосновано в §5 «Унаследовано»

python -m pytest tests_backend, полная матрица demo/smoke_*.mjs (223 файла), реальный мутационный прогон (--id=<mutant>, не только --check) — делта их не задевает, доказательства r1 остаются в силе (см. §5).

3. Проверка содержимого demo/smoke_space_copy.mjs (M3) — не только «файл существует»

Прочитал скрипт целиком, не только его зелёный результат:

  • Использует launch()/page.evaluate против production-бандла и реального demo-backend (card.hass.callWS обёрнут для перехвата порядка вызовов, но делегирует в baseCall — не мок с фиксированным ответом).
  • Проверяет порядок реальных WS-вызовов при Accept: acceptedCalls[0].type === 'houseplan/plan/optimize', acceptedCalls[1].type === 'houseplan/config/set' — не просто «вызов был», а именно последовательность (AC4/AC12).
  • Проверяет ноль записей при Cancel (cancelWrites.length === 0) — защитный AC5-класса случай подтверждён утверждением о нуле сайд-эффектов, а не отсутствием ошибки.
  • Проверяет форму итоговой копии предметно: rooms?.length === 0, wall_segments?.length === 0, partitions?.length > 0 — то есть именно AC6/AC9 (не room_drafts/wall_segments в копии), не только факт создания записи.
  • Проверяет позицию вставки: card._serverCfg.spaces[1]?.id === cleanCopy.id (сразу после источника, который является spaces[0]) — AC1/§7.1.
  • Проверяет предложенное имя proposedName.endsWith(' (2)') — тот же контракт, что и мутант space-copy-title-always-reuses-two, но через реальный DOM-рендер инпута, а не только через чистую функцию.

Прогон подтверждает результат:

{
 "copyButtonIsNeutralAndDefaultNameIsNumbered": true,
 "optimizeWarningNamesTheWholePlan": true,
 "optimizeCancelMakesZeroWritesAndKeepsName": true,
 "optimizeAcceptWritesInOrder": true,
 "acceptCreatesAndEntersRoomlessCopy": true,
 "cleanCopyNeedsNoExtraConfirmation": true,
 "cleanCopyIsInsertedAfterSourceAndSelected": true
}
OK

Это не тривиально-истинные ассерты: каждый сравнивает с конкретным значением/длиной/порядком, а не проверяет отсутствие исключения. Смок закрывает M3 как постоянный регрессионный барьер, а не как разовое подтверждение.

4. Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
M1 — check-docs.mjs красный, отпечаток скриншотов устарел Коммит 6dd9fb03 docs: refresh screenshot source fingerprint: пересчитан sourceFingerprint в docs/images/screenshots.json (eb0b787c… → 945121a2…). Проверено, что все 10 imageSha256 не изменились (git diff b286d7ba..6dd9fb03 — во всех сценах меняется только sourceSha256/шапка, imageSha256 идентичен) — правка чисто механическая, PNG не пересниманы заново без причины node scripts/check-docs.mjs → «Documentation checks passed (7 files, 12 external links)», прогнано мной заново на HEAD
M2 — golden:verify красный, space-room-color-popover-desktop-ru = different Коммит 7dd5b479 обновляет demo/golden/baselines/baselines-index.json (только хэш этой одной сцены: 9bbbcf9c… → 04bf325d…, проверено скриптом — из 153 записей scenarios изменилась ровно одна, ноль добавленных/удалённых сцен) и сам PNG-эталон. Коммит несёт обязательные Release: v1.72.0-beta.4 + Baseline-Reviewed: …/actions/runs/33948778925 (правило: коммит, трогающий demo/golden/baselines/**, обязан их нести — трейлер на месте, git show -s --format=full HEAD подтверждает порядок) npm run golden:verify → 153 passed, 0 different на полном матриксе (прогнано мной целиком, не только эта сцена — --scenario= на verify запрещён скриптом-политикой)
M3 — заявленный в ТЗ §17.6 browser smoke для Copy отсутствовал Коммит 7dd5b479 добавляет demo/smoke_space_copy.mjs (141 строка, разобран построчно в §3): clean-copy, Optimize→Cancel (0 записей), Optimize→Accept→переход — все три ветки из ТЗ §17.6 покрыты. Дополнительно 2c787913 актуализирует ожидания smoke_dialog_footer_width.mjs (3→4 кнопки в footer пространства) под новую кнопку «Копировать» node demo/smoke_space_copy.mjs → зелёный, все 7 полей true (см. §3); node demo/smoke_dialog_footer_width.mjs → зелёный, buttons: 4 для space/space_de desktop и narrow

Все три находки закрыты предъявленной строкой кода/данных и перепрогнанным гейтом, а не заявлением автора.

5. Унаследовано из r1 (без повторной проверки)

Документ: docs/reviews/CODE-REVIEW-456-r1.md, материал восстановлен по патч-эквивалентному коммиту b286d7ba (см. §0 — тот же diffstat, что 2c355ee9 в r1: 52 файла, src/space-copy.ts, src/space-copy-runtime.ts, src/plan-optimize-write.ts, src/space-dialog.ts, правки в houseplan-editor-runtime.ts/houseplan-card.ts, i18n×4).

  • Все 13 AC (AC1–AC13) — функциональная реализация не менялась дельтой; наследую вывод r1: 11 защитных AC подтверждены реальным прогоном мутантов (--id=<id>, не только --check), AC1/AC13 — прямым сравнением. Таблица «чем краснеет» из r1 §4 остаётся в силе, я лишь перепроверил --check (реестр валиден, все 11 id на месте) — не гонял мутанты по новой, т.к. src/space-copy*.ts не менялся.
  • Живой браузерный E2E прогон автора-предыдущего-раунда (кнопка → имя → Optimize warning → Accept → чистая копия) — принимаю как выполненный; теперь дополнительно закреплён постоянным demo/smoke_space_copy.mjs (это и есть закрытие M3, см. §4).
  • Backend: tests_backend/test_validation.py::test_issue_456_roomless_copy_with_rehosted_openings_is_valid — 378 passed, 4 skipped в r1, файл не тронут дельтой, не перепрогонял (песочница этого раунда тоже не несёт pytest/HA-стек — python3 -m pytest даёт No module named pytest, то же ограничение, что у r1).
  • Полная матрица demo/smoke_*.mjs (223 файла) и npm run invariants -- --config <export> — r1 обоснованно ограничился тремя показательными смоками общих примитивов (smoke_space_settings, smoke_danger_confirmation, smoke_optimize_geometry_preflight) плюс fixture-набором large-house; делта не трогает src/**, значит вывод smoke-select не мог измениться — перепроверил сам node scripts/smoke-select.mjs --base origin/dev --head HEAD: те же ~90 «прямых совпадений» из 223 по тем же общим полям рантайма (_frame, _modelCache, _path и т.п.), новых узко-специфичных совпадений на делту нет, т.к. делта не содержит символов рантайма.
  • Core-file budgets (houseplan-card.ts/houseplan-editor-runtime.ts на потолке) — не менялись дельтой, test/core-file-budget.test.mjs не тронут, npm test зелёный подтверждает, что потолок не пробит.
  • Единственная Low-находка r1 (фокус поля имени после Cancel не проверен) — снята решением ревьюера r1 без правки; дельта её не касается (demo/smoke_space_copy.mjs тоже не проверяет фокус, только значение и busy===false), риск тот же — оставляю снятой.
  • Быстрая проверка «одно число — один источник» для nextSpaceCopyTitle (не отдельная находка r1, но обязательный вопрос этого правила): функция вызывается один раз (space-copy-runtime.ts:66), результат кладётся в редактируемое поле диалога и это же значение уходит в запись при сабмите — нет второго независимого вычисления «предпросмотр vs запись». Файл дельтой не тронут, вывод переносится без изменений.

6. Находки этого раунда

Не найдено. Три Medium из r1 закрыты предъявленными строками и зелёными гейтами (§4); дельта не вносит нового кода в src/**/custom_components/**/*.py; трейлеры коммитов корректны (Issue: #456 на каждом, User-Visible: no везде — дельта не меняет пользовательское поведение, только доказательства/эталоны/документацию, changelog не тронут, что и требуется); коммит с golden-эталоном несёт Release:+Baseline-Reviewed:.

7. Что проверено и корректно (сверх наследования)

  • Ребейз на de30066b не создал скрытого конфликта: дифф de30066b..b286d7ba для scripts/mutation-gate.mjs показывает чистую дозапись 11 объектов в конец массива, без изменения инфраструктуры #451.
  • process-gate.mjs проходит на полном диапазоне origin/dev..HEAD (7 коммитов) без предупреждений.
  • Порядок трейлеров в git show -s --format=full HEAD — Issue/User-Visible/Release/Baseline-Reviewed, ничего не искажено ребейзом.

8. Чего не проверял и почему

  • test_ha_*.py (полный HA-harness) — как и в r1, песочница не несёт pinned Python 3.13/HA-стек; файл не тронут дельтой, риск не новый.
  • Реальный мутационный прогон --id=<id> для всех 11 мутантов заново — код src/space-copy*.ts не менялся дельтой (подтверждено diffstat b286d7ba неизменным), прогон дал бы тот же результат, что в r1 (уже реально исполнен и задокументирован там построчно); прогнал только дешёвый --check (реестр/уникальность якорей), это соразмерно объёму дельты (§2.10 «заново проверять только то, что задевает дельта»).
  • npm run invariants -- --config <export> — как и в r1, нет установки HA под рукой для реального config/get; геометрический код дельтой не тронут.
  • Полный запуск всех 223 смоков — предрелизный гейт (§8), делта не расширяет площадь src/**, вывод smoke-select не изменился (см. §5).

9. Вердикт

Все три Medium-находки r1 закрыты и перепроверены гейтами, а не заявлением автора: check-docs зелёный, golden:verify 153/153 (включая ранее «different» сцену), новый demo/smoke_space_copy.mjs — содержательный, прочитан построчно и подтверждён живым прогоном против реального demo-backend. Ребейз на ушедший на один коммит вперёд dev проверен контентно и не задевает код #456 (только независимая инфраструктура мутационного гейта #451) — полный разбор с нуля не требуется, дельта локальна. Новых находок нет. Функциональность (AC1–AC13) не менялась дельтой и наследуется из зелёного заключения r1 без повторного прогона мутантов.

Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0

Документ: docs/reviews/CODE-REVIEW-456-r2.md (публикуется шагом конвейера).


Материал раунда

  • Ветка: issue/456-copy-space, коммит 7dd5b4798a05 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 2221f7cc75b7c2d4a2077ed6a70f4c9e2042d3b0
    git log --all --format='%H %T' | grep 2221f7cc75b7
    
  • ТЗ docs/specs/456-copy-space.md, блоб a1def3eeefa15cac8b1bfe0542eb8d1ad0515fcf
    git log --all --find-object=a1def3eeefa15cac8b1bfe0542eb8d1ad0515fcf -- docs/specs/456-copy-space.md