Files
houseplan-card/docs/reviews/CODE-REVIEW-443-r1.md
T
2026-09-03 20:42:13 +00:00

244 lines
19 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-443-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/443
- Ветка: `issue/443-vacuum-route-polish`
- Материал: `git diff origin/dev...HEAD`, `HEAD = 1e5f33aa6413cb68659d5d1dcdb2b374a7404bda`
- ТЗ: `docs/specs/443-vacuum-route-polish.md`, ревью ТЗ зелёное на r2
(`docs/reviews/SPEC-REVIEW-443-r2.md`)
- Заход: r1 (первый код-ревью цикл этой задачи)
## Скоуп диффа
Один коммит поведения `cebd20a9` (`fix: polish vacuum map routes`) плюс
promotion-коммит `1e5f33aa` (пересборка бандла и канонических скриншотов).
Три независимых правки, всё согласно контракту ТЗ §1–§4:
1. `map_routes` authority — `src/vacuum-routes.ts::effectiveRoutes()`,
`custom_components/houseplan/vacuum_routes.py::effective_routes()`:
условие `Array.isArray(x) && x.length` → `Array.isArray(x)`, отдельно на
frontend и backend.
2. Single-space export — `custom_components/houseplan/import_export.py`:
`kept_routes or None` → `kept_routes` (пустой список пишется как есть).
3. Группа «Пространство удалено» — `src/editors/vacuum-maps-section.ts`:
разбиение `routes` на `validRows`/`missingRows`, отдельная секция с
локализованным заголовком; `src/i18n/support/{en,ru,de,fr}.json`,
`src/styles/dialogs.styles.ts`.
4. Vacuum-only render snapshot — `src/render-device-snapshot.ts` (`vacuumDevices`
subset, заморожен), `src/houseplan-card.ts` (`_renderVacuumDevices` getter и
новый call site `_renderVacuums(this._renderVacuumDevices, …)`).
Плюс тесты/мутанты (`test/vacuum-routes.test.mjs`,
`test/render-device-snapshot.test.mjs`, `test/isometric-contract.test.mjs`,
`tests_backend/test_vacuum_routes.py`, `tests_backend/test_ha_import_export.py`,
`scripts/mutation-gate.mjs`, `demo/smoke_vacuum_route_draft.mjs`) и документация
(`docs/VACUUM.md`, `docs/CONFIG-COMPATIBILITY.md`, `docs/USER-GUIDE.ru.md`, оба
changelog) — все пять release-артефактов, названных ревью ТЗ, на месте.
## Как проверялось
**Дешёвые гейты подтверждены на этом SHA, не перегонялись.** Validate зелёный
на точном `1e5f33aa`
(https://github.com/Matysh/houseplan-card/actions/runs/33802298798):
джобы `frontend` (typecheck/unit/mutation-gate/bundle:sync),
`backend` (pytest в HA), `golden`, `performance_smoke`, все 3 шарда `smoke`,
`docs`, `provenance`, `process-gate`, `hassfest`, `hacs` — все `success`, я
сверил через `gh run view --json jobs` лично, не только со слов автора.
Отдельно проверены с сохранением headSha:
- полный performance-прогон (7 профилей, включая large-house 60/200) —
`33802485595`, `headSha = 1e5f33aa…`, success;
- каноническая съёмка документации — `33802095608`, success (на SHA
промежуточного дерева `1fd042d9`, откуда получены новые PNG, зафиксированные
затем в promotion-коммите `1e5f33aa`; сам `docs` job Validate подтверждает
fingerprint против финального дерева отдельно и тоже зелёный).
Гейты, которые прогнал сам ревьюер (дёшево, воспроизводимо):
| Гейт | Команда | Результат |
|---|---|---|
| smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 6 прямых совпадений: `smoke_cold_view_vacuum`, `smoke_controls`, `smoke_glow_fail_dark`, `smoke_glow_geometry_resilience`, `smoke_vacuum_firstuse`, `smoke_vacuum` — все входят в полный набор, уже прогнанный зелёным в Validate (3/3 шарда) |
| no-new-any | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (83 добавленные строки в 5 файлах) |
Не прогонял: `npx tsc --noEmit`, `npm test`, `npm run build` — покрыты Validate
на этом же SHA (условие задачи: не гонять то, что уже зелёное на точном SHA).
`npm run model-invariants` не запускался — дифф не трогает рёбра комнат, записи
толщины, `layout`, `marker.space` (сравнивается `route.space`, поле маршрута
робота, не геометрия) или `open_spans`; гейт неприменим. `python -m pytest
tests_backend -q` не гонял отдельно — покрыт зелёным backend-job Validate на
этом SHA. Полный `golden:capture`/локальный HA-harness не запускал — канон
Linux CI зелёный на точном SHA, второй прогон ничего не добавляет.
## Разбор по AC
**AC1 (единая route authority).** `test/vacuum-routes.test.mjs` добавляет
`explicit empty routes remain authoritative over legacy calibration` — три
состояния: absent (legacy), `null` (legacy), `[]` (пусто, без legacy). Защитный
AC, доказан таблицей «чем краснеет»:
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC1 (frontend) | `node --test --test-name-pattern="explicit empty routes remain authoritative" test/vacuum-routes.test.mjs` | мутант `vacuum-empty-routes-revive-legacy-frontend` возвращает `explicit.length`, гейт красный (проверено по определению мутанта в `scripts/mutation-gate.mjs:384-396`, сам mutation-gate job зелёный в Validate — значит применённая мутация действительно ловится) |
| AC2 (backend) | `tests_backend/test_vacuum_routes.py::test_explicit_empty_routes_remain_authoritative` | мутант `vacuum-empty-routes-revive-legacy-backend` (то же условие в Python) |
| AC3 (export) | `tests_backend/test_ha_import_export.py::test_issue_443_space_export_preserves_explicit_empty_routes` — экспортирует marker с explicit routes, полностью отфильтрованными по space, и уцелевшей legacy `calibration`; проверяет `map_routes == []`, `effective_routes(...) == []` после round-trip | мутант `vacuum-space-export-drops-empty-authority` возвращает `kept_routes or None` |
Читкой подтверждено: `writeRoutes()` в `vacuum-maps-section.ts:129`
(`explicit ? (vacuum.map_routes ?? null) : null`) при `map_routes: []` берёт
пустой массив как базу (`![]` есть `false` в JS — ветка `convertLegacyRoutes`
не выполняется), то есть UI-редактирование поверх уже-пустого explicit-состояния
не оживляет legacy при следующей записи. Отдельного unit на этот конкретный
путь нет, но `demo/smoke_vacuum_route_draft.mjs:50-72` стартует именно с
`map_routes: []` и успешно проводит через него draft-flow — риск низкий, тот же
`explicit`-флаг, что доказан AC1/AC2.
**AC2 (backend parity).** См. таблицу выше; TS и Python проверяют одну и ту же
четырёхстрочную матрицу independently, значения совпадают (absent/null → 1
маршрут, `[]` → 0).
**AC3 (export).** См. таблицу; дополнительно `assert
document["transfer"]["dropped_marker_links"] == 1` — счётчик не подменяется
побочно.
**AC4 (группа «Пространство удалено»).** Контракт §3 — не защитный AC
(расположение/текст/группировка), доказательство обычным сравнением достаточно.
`demo/smoke_vacuum_route_draft.mjs:150-192` (часть полного зелёного smoke-прогона
в Validate) проверяет: ровно одна группа
(`[data-hp="vacuum-route-missing-group"]`), заголовок `"Deleted space"`,
валидные строки идут первыми и не входят в группу, три missing-строки
отсортированы по идентичности (`missing-a,missing-b,missing-z` — по
`map_id`→`source`, а не по `space`, соответствует §3.2), каждая строка группы
сохраняет `<select>` и обе кнопки (edit/delete остаются доступны), warning
(`.warn`) не исчезает, заголовок читаем в light и dark темах. Pending draft
рендерится отдельно от `validRows`/`missingRows` (`vacroute.pending` вне обеих
групп) — контракт #441 не нарушен.
**AC5 (renderer получает только роботов).**
`test/render-device-snapshot.test.mjs` — `vacuumDevices` содержит только
устройство с фактом `vacuum:<id>`, переиспользует тот же клонированный элемент
(`snapshot.vacuumDevices[0] === snapshot.devices[1]`), заморожен
(`Object.isFrozen`, `push` бросает `TypeError`). Call site подтверждён
структурным свидетелем: `test/isometric-contract.test.mjs:37` требует
буквальное вхождение `_renderVacuums(this._renderVacuumDevices, view,
space.id)` в `src/houseplan-card.ts` — до правки строка была
`_renderVacuums(this._renderDevices, …)`, то есть тест механически падает при
откате call site, не только при откате subset-логики. Проверено чтением:
`_captureRenderDeviceSnapshot()` (`houseplan-card.ts:4776-4822`) итерирует
`this._devices` (полный ростер, не отфильтрованный по текущему пространству) и
кладёт `facts.set('vacuum:'+id, …)` для каждого `_isVacDev(device)` — то есть
subset действительно содержит **всех** роботов плана, а не только текущего
этажа, что и требует AC6.
**AC6 (межэтажное и snapshot-поведение).** Существующий mutation-тест
`vacuum-overlay-back-to-the-dock-space-filter` (`scripts/mutation-gate.mjs:423`)
обновлён на новый call site (`find` теперь ищет `_renderVacuumDevices`) и
по-прежнему требует падения `smoke_vacuum_multifloor` при подмене на
current-space `devs`. `space-card.ts` (второй, статический card) не вызывает
`_renderVacuums` вовсе — никакой параллельный call site не остался
непереключённым.
Слабое место: мутант ловит подмену на `devs` (устройства текущего
пространства), но не ловит гипотетический откат `_renderVacuumDevices` →
`this._renderDevices` (полный ростер всех этажей) — такой откат не сломал бы ни
один функциональный тест, только регрессировал бы производительность к
исходной проблеме (в). Эту дыру закрывает не mutation-gate, а структурный
witness `isometric-contract.test.mjs:37`: он проверяет точную подстроку
`_renderVacuumDevices`, поэтому откат к `_renderDevices` уронит и его. Тест
умеет падать — проверено чтением diff (до правки эта точная строка не
существовала).
**AC7 (производительность).** Full performance run зелёный на точном SHA,
включает large-house 60/200 (см. «Как проверялось»). Structural witness — тот
же `isometric-contract.test.mjs:37` плюс `render-device-snapshot.test.mjs`,
подтверждающий `V < N` состав subset. Новый latency-бюджет не вводился —
согласовано ТЗ и не оспаривается ревью.
**AC8 (совместимость и документация).** `docs/VACUUM.md`,
`docs/CONFIG-COMPATIBILITY.md`, `docs/USER-GUIDE.ru.md` обновлены текстуально
верно (сверено чтением — формулировки описывают ровно новое поведение, не
больше и не меньше). Оба changelog правлены в том же коммите `cebd20a9`, что и
поведение (`User-Visible: yes`) — правило §10.2.4 соблюдено. i18n: все четыре
словаря (`en/ru/de/fr`) получили `vac.route_missing_space_group`,
`test/i18n.test.mjs` обновлён на `keys.length === 78` — тест самих ключей
проверяет полноту, я прочитал все четыре файла и подтвердил перевод
присутствует и осмыслен.
## Одно число — один источник
Дифф не вводит и не показывает пользователю новую числовую величину дважды:
меняется правило чтения списка (authority) и группировка строк, оба — не
числа. Проверять нечего.
## Что проверено и корректно
- Frontend/backend parity по всей 4-строчной матрице authority (AC1/AC2),
включая отрицательные мутанты на обеих сторонах.
- Export не теряет explicit empty (AC3), включая проверку `effective_routes`
после round-trip и неизменности счётчика `dropped_marker_links`.
- Группировка потерянных маршрутов: единственная группа, стабильный порядок,
строки остаются редактируемыми/удаляемыми, warning сохранён, light/dark
читаемость (AC4).
- Vacuum-only snapshot: immutable subset, реюз тех же клонированных строк,
верный call site, подтверждено чтением что capture обходит весь ростер, а не
только текущее пространство (AC5/AC6).
- `writeRoutes()` в редакторе корректно трактует уже-пустой explicit массив как
базу без отката к legacy-конверсии (проверено чтением).
- i18n-словари всех 4 языков и test/i18n.test.mjs согласованы.
- Документация (`VACUUM.md`, `CONFIG-COMPATIBILITY.md`, `USER-GUIDE.ru.md`) и
оба changelog обновлены в том же коммите, что и поведение.
- `no-new-any` — новых `any` нет (проверено лично).
- Все дорогие гейты (typecheck/unit/build/mutation/backend/golden/performance/
docs/smoke×3) зелёные на точном HEAD SHA `1e5f33aa` — подтверждено запросом
к самому workflow run, а не только по ссылке из хендоффа.
## Находки
Находок нет. High: 0, Medium: 0, Low: 0.
Единственное отмеченное выше наблюдение (мутация не покрывает гипотетический
откат `_renderVacuumDevices` → `_renderDevices`) не поднимается до Low: реальный
свидетель (структурный тест на точную подстроку call site) уже существует и
доказанно падает при этом откате — это другой инструмент (source-match, не
mutation-gate), но не пробел в покрытии.
## Чего не проверял
- Не запускал `npx tsc --noEmit`, `npm test`, `npm run build`,
`npm run bundle:sync`, `node scripts/mutation-gate.mjs --check`,
`npm run golden:verify`, `python -m pytest tests_backend -q`,
performance-профили и `node scripts/check-docs.mjs` лично — все подтверждены
зелёными на точном SHA `1e5f33aa` в трёх названных прогонах CI, которые я
сверил через `gh run view` (headSha, jobs, conclusion), а не принял на слово
из хендоффа.
- Не запускал `node scripts/model-invariants.mjs` — дифф не касается геометрии
(рёбра комнат, толщина, `layout`, `marker.space`, `open_spans`); гейт не
применим по критерию задачи.
- Не проводил ручного тестирования в браузере (в этом процессе такой фазы нет);
AC4/AC5/AC6/AC7 разобраны по коду и по прогону named browser smokes/perf
runs в CI.
- Не проверял 6 «прямых совпадений» smoke-select индивидуально локально — они
часть полного набора, уже зелёного во всех 3 шардах Validate на этом SHA;
повторный локальный прогон не добавил бы информации.
## Материал раунда
- SHA ветки на момент вердикта: `1e5f33aa6413cb68659d5d1dcdb2b374a7404bda`
(сверено `git rev-parse HEAD` непосредственно перед выводом).
- Дерево материала: вывод `git diff origin/dev...HEAD` на этом SHA.
- Validate CI: https://github.com/Matysh/houseplan-card/actions/runs/33802298798
(`headSha` подтверждён равным `1e5f33aa…`).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/443-vacuum-route-polish`, коммит `1e5f33aa6413` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `9606928137c6ead0bada1645f26c70ca819dd342`
```
git log --all --format='%H %T' | grep 9606928137c6
```
- ТЗ `docs/specs/443-vacuum-route-polish.md`, блоб `eb5bc721764abebb38a642cf9e450f52c6e608d7`
```
git log --all --find-object=eb5bc721764abebb38a642cf9e450f52c6e608d7 -- docs/specs/443-vacuum-route-polish.md
```