mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
214 lines
20 KiB
Markdown
214 lines
20 KiB
Markdown
# CODE-REVIEW-244-r2
|
||
|
||
- Issue: [#244](https://github.com/Matysh/houseplan-card/issues/244)
|
||
- Этап: code (PROCESS.md §2.7)
|
||
- Заход: r2 · блокирующих циклов израсходовано 1 из 4
|
||
- Кандидат (HEAD): `a9466da649c5a24ad84cc79b0e6389b724dc8087`
|
||
- SHA предыдущего раунда (r1, красный): `a09338f` (`build: refresh issue 244 bundle fingerprints`)
|
||
- Дельта разбора: `git diff a09338f..HEAD`
|
||
|
||
## Скоуп раунда
|
||
|
||
r1 (2026-08-22T19:55:17Z, `docs/reviews/CODE-REVIEW-244-r1.md`) дал красный
|
||
вердикт на кандидате `a09338f`: High: 2, Medium: 0.
|
||
|
||
- **H1** — именованный в AC смок `demo/smoke_orphan_space_references.mjs`
|
||
красный из-за дефекта тестовой фикстуры (Optimize→Undo и delete-blocker
|
||
сценарии делили один и тот же маркер).
|
||
- **H2** — регрессия контракта #113: новый безусловный preflight в
|
||
`_deleteSpace()`/`_space_delete_candidate()` блокировал удаление
|
||
единственного оставшегося пространства с хотя бы одним активным маркером,
|
||
оставляя пользователя без пути в состояние «Пусто».
|
||
|
||
Между r1 и r2 автор:
|
||
|
||
1. исправил фикстуру H1 (`389baca`);
|
||
2. вынес H2 на арбитраж владельца, получил явное решение (комментарий
|
||
`2026-08-22T21:07:34Z`: узкое исключение для единственного оставшегося
|
||
пространства, снимаются только `space`/`room_id`, binding/icon/actions
|
||
сохраняются, при 2+ пространствах блокировка остаётся);
|
||
3. реализовал решение (`74f03bf` — продуктовый код, тесты, ТЗ, оба
|
||
changelog, USER-GUIDE.md/.ru.md, CONFIG-COMPATIBILITY.md, TESTING.md;
|
||
`a9466da` — принят docs-скриншот-фингерпринт).
|
||
|
||
Дельта не ребейзилась на ушедший вперёд `dev`, не меняет контракт поведения
|
||
за пределами явно арбитрированного исключения и не задевает новую
|
||
подсистему — она локальна к сценарию удаления пространства (frontend
|
||
`_deleteSpace`/`space-deletion.ts`, backend `_space_delete_candidate`/
|
||
`ws_space_delete`, их тесты и синхронизированная документация). Поэтому
|
||
разбор r2 ограничен этой дельтой; остальное наследуется из r1 без повторной
|
||
проверки (раздел ниже).
|
||
|
||
## Как проверялось
|
||
|
||
Прочитан полный `git diff a09338f..HEAD` (19 файлов, +587/-62) построчно:
|
||
`src/houseplan-card.ts` (`_deleteSpace`), `src/space-deletion.ts`
|
||
(`createSpaceDeletionCandidate`/`collectSpaceMarkerDependencies`),
|
||
`custom_components/houseplan/websocket_api.py`
|
||
(`_space_delete_candidate`/`ws_space_delete`), оба новых теста
|
||
(`test/space-deletion.test.mjs`, `tests_backend/test_ha_websocket.py`), обе
|
||
правки смоков, обновления `docs/specs/244-orphan-space-references.md` (§2,
|
||
§3, §8, §10, таблица AC9/AC10) и синхронные правки
|
||
`docs/CHANGELOG.md`/`.ru.md`, `docs/USER-GUIDE.md`/`.ru.md`,
|
||
`docs/CONFIG-COMPATIBILITY.md`, `docs/TESTING.md`.
|
||
|
||
Frontend (`space-deletion.ts`) и backend (`websocket_api.py`) реализации
|
||
проверены на структурную симметрию — они дублируют одну и ту же логику
|
||
(`deletingLastSpace` / `deleting_last_space`, три источника
|
||
`referencesDeletedSpace`: `marker.space`, `marker.room_id` через
|
||
`roomIds`, собственная позиция маркера в `layout`) и совпадают построчно
|
||
по смыслу.
|
||
|
||
### Гейты — прогнаны
|
||
|
||
| Гейт | Результат |
|
||
|---|---|
|
||
| `npx tsc --noEmit` | PASS, без вывода |
|
||
| `npm test` | 1113/1113 PASS (было 1112 на кандидате r1 — дельта добавила 1 тест в `test/space-deletion.test.mjs`) |
|
||
| `npm run build` + сверка трёх копий бандла | PASS; `dist/houseplan-card.js`, `demo/srv/assets/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js` — идентичный SHA-256 `02f6585f7df6fd70005e78ead84f50d99eb843f8fae64119fd4e98c0232e2f9e`, совпадает с заявленным в хендоффе |
|
||
| `node scripts/check-docs.mjs` | PASS: «Documentation checks passed (7 files, 10 external links)» — фингерпринт скриншотов из `a9466da` принят |
|
||
| `python -m pytest tests_backend -q` | В песочнице ревью изначально не было установлено `pytest-homeassistant-custom-component`/`homeassistant` (в отличие от заявления «зависимости уже установлены workflow»); установил тем же набором пакетов, что и CI-workflow (`pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend`), после чего прогнал полный набор: **352 passed, 1 error**. Ошибка — `test_ha_upload.py::test_upload_ok`, `AssertionError` на `threading._DummyThread` при остановке hass; не связана с диффом (дельта `websocket_api.py` его не касается), и в r1 уже была независимо воспроизведена на чистом `origin/dev`. Два новых теста issue 244 (`test_issue_244_last_occupied_space_candidate_detaches_all_affected_markers`, `test_issue_244_last_occupied_space_delete_preserves_marker_records`) — в числе прошедших |
|
||
| `git diff --check a09338f..HEAD` | чисто, без вывода |
|
||
|
||
### Смоки — прогнаны выборочно
|
||
|
||
`node scripts/smoke-select.mjs --base a09338f --head HEAD`: 2 символа на
|
||
изменённых строках, матрица 169 смоков. Инструмент не выдал ни «прямого
|
||
совпадения», ни «зарегистрированной связи» — только «слабую связь» (общее
|
||
имя `_spaceDialog`, 23 смока) и явную «НЕОПРЕДЕЛЁННОСТЬ». Слабая связь сама
|
||
по себе не обязывает прогонять все 23 — но два смока из этого списка прямо
|
||
названы в таблице AC10 ТЗ как источник доказательства
|
||
(`smoke_orphan_space_references`, `smoke_optional_space_model`), поэтому
|
||
они прогнаны целиком, а не выборочно по имени:
|
||
|
||
- `node demo/smoke_orphan_space_references.mjs` → все 10 ассертов `true`,
|
||
включая `deleteExplainsBlockerWithoutConfirmOrWrite: true` — это прямое
|
||
закрытие H1 (в r1 этот же ассерт был `false`).
|
||
- `node demo/smoke_optional_space_model.mjs` → все 11 ассертов `true`,
|
||
включая новые `deleteLastUsesAuthoritativeEndpoint: true` и
|
||
`deleteLastPreservesMarkersWithoutPlacement: true` — прямое закрытие H2
|
||
(в r1 5 из 9 ассертов этого смока были красными на том же кандидате).
|
||
|
||
Остальные 21 смок из «слабой связи» не прогонялись: дельта не трогает их
|
||
собственные сценарии (цветовые пикеры, шрифты, слои, сохранённые планы и
|
||
т.д.), связь с `_spaceDialog` объясняется тем, что это широкое общее поле
|
||
диалога, через которое проходит любой код редактора пространства — не
|
||
специфика этой правки. `smoke_optimize_geometry_preflight` и
|
||
`smoke_fixed_floor`, упомянутые в хендоффе автора, дельтой r2 не задеты
|
||
(они закрывают AC7/AC11, не AC9/AC10) и не перепрогонялись — решение по
|
||
объёму, не пропуск.
|
||
|
||
### Гейты — не прогнаны (и почему)
|
||
|
||
- `npm run golden:verify` — дельта не меняет рендер, геометрию, стили или
|
||
слои; она меняет только данные (`marker.space`/`room_id`, `layout`) и
|
||
порядок веток в preflight/backend-транзакции. Визуальный результат
|
||
строки AC не заявляет.
|
||
- Mutation-тестирование — не названо в AC9/AC10/AC12 для этой правки;
|
||
неизменность входа уже утверждается напрямую в обоих новых unit-тестах
|
||
(`config.markers[0].space === 'only'`, `layout.direct.s === 'only'`,
|
||
`config["markers"][0]["space"] == "only"`, `layout["direct"]["s"] == "only"`
|
||
после вызова). Это предрелизный гейт, не гейт ревью.
|
||
- performance-профили — путь не является горячим (однократная операция по
|
||
явному действию пользователя), в AC не назван.
|
||
- Полный browser smoke (167 сценариев) — дельта локальна к одному
|
||
сценарию (`_deleteSpace`), `smoke-select` не выдал широкого совпадения;
|
||
полный прогон — предрелизный гейт (PROCESS.md §8).
|
||
|
||
## Находки
|
||
|
||
Нет. H1 и H2 из r1 закрыты; новых High/Medium в дельте не найдено.
|
||
|
||
## Закрытие раунда r1
|
||
|
||
| Находка r1 | Чем закрыта | Где это видно |
|
||
|---|---|---|
|
||
| **H1** — `demo/smoke_orphan_space_references.mjs` красный (`deleteExplainsBlockerWithoutConfirmOrWrite: false`) из-за общего маркера между Optimize→Undo и delete-blocker сценариями | Коммит `389baca`: в фикстуру добавлено отдельное пространство `other` (делает `home` не последним) и отдельный маркер `home-blocker`, который единственный реально блокирует удаление `home`; маркер `orphan` из Optimize→Undo сценария больше не пересекается с ним | `git diff a09338f..HEAD -- demo/smoke_orphan_space_references.mjs`; локальный прогон смока — `deleteExplainsBlockerWithoutConfirmOrWrite: true`, остальные 9 ассертов тоже `true` |
|
||
| **H2** — безусловный preflight блокирует удаление единственного оставшегося занятого пространства, ломая empty-state контракт #113 | Владелец арбитрировал узкое исключение (комментарий `2026-08-22T21:07:34Z`); реализовано в `74f03bf`: `deletingLastSpace`/`deleting_last_space` в `src/houseplan-card.ts`, `src/space-deletion.ts`, `custom_components/houseplan/websocket_api.py` — при единственном пространстве preflight и backend-guard больше не блокируют удаление по dependencies, атомарно снимают `space`/`room_id` у затронутых active/removed маркеров, остальные поля не трогают; ТЗ §3/§10/AC9/AC10, оба changelog, `USER-GUIDE.md/.ru.md`, `CONFIG-COMPATIBILITY.md`, `TESTING.md` синхронно обновлены в том же коммите (`User-Visible: yes`) | `git diff a09338f..HEAD -- src/houseplan-card.ts src/space-deletion.ts custom_components/houseplan/websocket_api.py docs/specs/244-orphan-space-references.md`; новые тесты `test/space-deletion.test.mjs` («deleting the last occupied space detaches placement but preserves markers») и `tests_backend/test_ha_websocket.py` (`test_issue_244_last_occupied_space_candidate_detaches_all_affected_markers`, `test_issue_244_last_occupied_space_delete_preserves_marker_records`) — все PASS; смок `smoke_optional_space_model` — `deleteLastUsesAuthoritativeEndpoint: true`, `deleteLastPreservesMarkersWithoutPlacement: true` |
|
||
|
||
## Унаследовано из r1 (без повторной проверки)
|
||
|
||
Документ: `docs/reviews/CODE-REVIEW-244-r1.md`, SHA кандидата `a09338f`.
|
||
|
||
- Диагноз §3 ТЗ и его соответствие коду (`resolveExplicitMarkerPlacement`,
|
||
приоритет `marker.space` для virtual-маркера, фильтр рендера View/Static,
|
||
`_deleteSpace()` до правки, `build_space_merge()`, `resolveInitialSpace()`).
|
||
- Optimize-репаир ссылок (signature-remap, effective HA Area, safe-detach
|
||
для нерешённых, `nestedRefsUnresolved`, `vacuum.segment_map`, room-label
|
||
positions) — AC1–AC8, AC12, AC13, AC14 из таблицы ТЗ; дельта r2 их не
|
||
трогает.
|
||
- Import одного пространства чинит существующие target-orphan ссылки по
|
||
`id_map` (AC8) — не задет.
|
||
- Валидация `default_floor` в редакторе карточки, RU/EN inline-ошибка,
|
||
безопасный fallback на первое пространство (AC11) — не задета.
|
||
- Гейты `tsc --noEmit`, `npm run build`+сверка бандла,
|
||
`node scripts/check-docs.mjs`, backend pytest (кроме двух новых тестов),
|
||
`git diff --check` — уже были зелёными на `a09338f` для той части
|
||
дифф-объёма, которую r2 не менял; в r2 они перепрогнаны заново целиком
|
||
(см. таблицу гейтов выше) как более дешёвый путь, чем вычленять
|
||
неизменную часть, но результат по неизменным файлам совпадает с r1.
|
||
- Touch/kiosk паритет View/Static-рендера (закрыт ещё на этапе spec,
|
||
SPEC-REVIEW-244-r2) — реализация r1/r2 не меняет рендер-путь, вывод
|
||
наследуется без повторной проверки.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Frontend (`space-deletion.ts`) и backend (`websocket_api.py`) реализации
|
||
исключения для последнего пространства структурно симметричны и
|
||
одинаково определяют «затронутый» маркер: прямая ссылка `space`, `room_id`
|
||
комнаты удаляемого пространства, либо собственная позиция в `layout` с
|
||
`s == spaceId`.
|
||
- Затронутым маркерам снимаются ровно `space` и `room_id`; `binding`,
|
||
`icon`, `actions`, `description` и прочие поля не трогаются — проверено
|
||
и unit-тестами (icon/actions сохранены), и backend-тестом полного
|
||
жизненного цикla через реальный `hass_ws_client`.
|
||
- Незатронутые маркеры с уже орфанной ссылкой (`space: 'legacy'` в тесте) не
|
||
переписываются — соответствует AC12 («уже валидные... не входит» — здесь
|
||
наоборот, уже-невалидная чужая ссылка тоже не тронута, это не откат
|
||
выполненного AC12, а сохранение чужого, не принадлежащего удаляемому
|
||
пространству, состояния).
|
||
- При 2+ пространствах старое поведение блокировки не изменилось: тест
|
||
`test_issue_244_space_delete_dependency_and_tombstone_candidate` (не
|
||
тронут диффом) и первая половина `smoke_orphan_space_references`
|
||
(`deleteExplainsBlockerWithoutConfirmOrWrite`) подтверждают, что
|
||
исключение сработало только для `deleting_last_space`.
|
||
- UI-путь: `_deleteSpace()` при `deletingLastSpace` пропускает диалог
|
||
блокеров и идёт к обычному `confirm()`, при неудаче (`conflict`/
|
||
`space_in_use`) пересчитывает `stillLastSpace` по свежим данным — гонка,
|
||
где параллельно появилось второе пространство или новый маркер, снова
|
||
корректно показывает blockers вместо тихого отказа.
|
||
- Backend-транзакция остаётся под `write_lock`, ревалидирует обе revision
|
||
и пишет config/layout атомарно — новый код встроен в существующий
|
||
guard, не ослабляет его (проверено чтением, дополнительно подтверждено
|
||
прогоном `test_issue_244_space_delete_is_authoritative_and_revision_guarded`,
|
||
не тронутого диффом, и новым
|
||
`test_issue_244_last_occupied_space_delete_preserves_marker_records`).
|
||
- Документация (ТЗ §2/§3/§8/§10/таблица AC, оба CHANGELOG, оба USER-GUIDE,
|
||
CONFIG-COMPATIBILITY.md, TESTING.md) синхронно и без противоречий
|
||
описывает именно реализованное исключение — совпадает построчно с
|
||
логикой кода, а не только с решением владельца в общих словах.
|
||
- Трейлеры: `74f03bf` (`User-Visible: yes`) содержит правки обоих
|
||
changelog в том же коммите; `389baca` и `a9466da` — `User-Visible: no`,
|
||
соответствует их содержанию (тестовая фикстура и docs-фингерпринт).
|
||
|
||
## Что не проверялось (и почему)
|
||
|
||
- Полный browser smoke (167 сценариев) — не запускался; выборка через
|
||
`smoke-select.mjs` не дала оснований для полного прогона, дельта
|
||
локальна к одному сценарию. Это предрелизный гейт (PROCESS.md §8).
|
||
- `npm run golden:verify` — не запускался, дельта не меняет визуальный
|
||
результат (см. таблицу гейтов).
|
||
- Mutation-тестирование — не запускалось, не в AC для этой правки,
|
||
неизменность входа уже прямо утверждается в новых unit/backend тестах.
|
||
- Performance-профили — не запускались, путь не горячий и не назван в AC.
|
||
- Ручное тестирование в браузере (открыть карточку, вживую удалить
|
||
единственное пространство) — не проводилось; вместо этого прогнаны
|
||
browser-смоки headless (`smoke_orphan_space_references`,
|
||
`smoke_optional_space_model`), которые управляют тем же
|
||
`HouseplanCard`-элементом через реальный DOM/Lit-рендер в Puppeteer, а
|
||
не мокают его — это и есть браузерное доказательство для AC9/AC10 по
|
||
их собственному способу доказательства из таблицы ТЗ.
|
||
- Полный обзор кода Optimize/import/`default_floor` (§8, §9, §11 ТЗ) —
|
||
не перечитывался заново; наследуется из r1 (см. раздел выше), дельта
|
||
r2 эти файлы не трогает.
|