19 KiB
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:
map_routesauthority —src/vacuum-routes.ts::effectiveRoutes(),custom_components/houseplan/vacuum_routes.py::effective_routes(): условиеArray.isArray(x) && x.length→Array.isArray(x), отдельно на frontend и backend.- Single-space export —
custom_components/houseplan/import_export.py:kept_routes or None→kept_routes(пустой список пишется как есть). - Группа «Пространство удалено» —
src/editors/vacuum-maps-section.ts: разбиениеroutesнаvalidRows/missingRows, отдельная секция с локализованным заголовком;src/i18n/support/{en,ru,de,fr}.json,src/styles/dialogs.styles.ts. - Vacuum-only render snapshot —
src/render-device-snapshot.ts(vacuumDevicessubset, заморожен),src/houseplan-card.ts(_renderVacuumDevicesgetter и новый 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; самdocsjob 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лично — все подтверждены зелёными на точном SHA1e5f33aaв трёх названных прогонах 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…).
Материал раунда
- Ветка:
issue/443-vacuum-route-polish, коммит1e5f33aa6413— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
9606928137c6ead0bada1645f26c70ca819dd342git log --all --format='%H %T' | grep 9606928137c6 - ТЗ
docs/specs/443-vacuum-route-polish.md, блобeb5bc721764abebb38a642cf9e450f52c6e608d7git log --all --find-object=eb5bc721764abebb38a642cf9e450f52c6e608d7 -- docs/specs/443-vacuum-route-polish.md