docs: review document for #443

Issue: #443
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-03 20:42:13 +00:00
parent 1e5f33aa64
commit b7b805f2af
+243
View File
@@ -0,0 +1,243 @@
# 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
```