25 KiB
Код-ревью #162 — заход r1
- Issue: #162
- Этап: code (PROCESS.md §2.7)
- Ветка:
issue/162-vacuum-map-space-routing, HEAD4032b810 - ТЗ:
docs/specs/162-vacuum-map-space-routing.md, утверждено на редакции 2 / spec-review r2, SHA4c56b389 - Заход: r1 (первый код-ревью для этого issue) · блокирующих циклов израсходовано 0 из 4
Вердикт
Красный. High: 1, Medium: 1 (оба в скоупе задачи, чинятся в ней же).
Скоуп проверки
Материал: git log --oneline origin/dev..HEAD (11 коммитов) и
git diff origin/dev...HEAD (71 файл). Ревью полное (не дельта): это первый
код-ревью цикл для #162.
Прочитано и сверено построчно:
src/vacuum-routes.tsи питоновское зеркалоcustom_components/houseplan/vacuum_routes.py— типы,validateMarkerRoutes/validate_marker_routes,effectiveRoutes/effective_routes,resolveRoute/resolve_route,adoptLegacyRun/adopt_legacy_run,planVacuumOverlay,routeWarningKey;src/vacuum-route-edit.ts—addRoute,removeRoute,changeRouteSpace,saveRouteCalibration,convertLegacyRoutes,writeVacuumMatrix,planVacuumFit,calibrationTarget;src/houseplan-card.ts— снимок фактов (facts.set('vacuum:<id>', …)), новая_renderVacuums(рендер поroute.space, а не по фильтру устройств дока),_vacRouteBadge(amber warning);src/houseplan-editor-runtime.ts—_vacSaveMatrix,_vacAutoCalibrate,_vacApplyCalibrationProposal,_vacStartFit,_vacFitSave(весь путь калибровки);src/editors/vacuum-maps-section.ts— весь UI блока «Карты и этажи»;src/space-deletion.ts—routesIntoSpace,collectSpaceMarkerDependencies,createSpaceDeletionCandidate(AC16);custom_components/houseplan/trails.py—same_run_identity,can_resume_trail_run,TrailBook.on_point/drop_unknown_routes,TrailRecorder.async_refresh/async_purge_orphans/_route_id;custom_components/houseplan/validation.py—validate_marker_vacuum_routes(change-aware, как badge-валидатор) и её проводка вwebsocket_api.py(config/set, optimize) иimport_export.py(оба import flow);scripts/mutation-gate.mjs— определения восьми заявленных + новых мутантов (14 по факту), два прогнаны лично (см. ниже);docs/CHANGELOG.md/.ru.md,docs/VACUUM.md,docs/USER-GUIDE.ru.md,docs/CONFIG-COMPATIBILITY.md,docs/ARCHITECTURE.md,docs/TESTING.md.
Не читал построчно (низкий риск, не в фокусе AC): i18n-словари полностью
(точечно сверил RU/EN парность нужных ключей), docs/images/screenshots.json
(бинарный/сгенерированный артефакт), dist/** и
custom_components/houseplan/frontend/** (класс D, генерируются билдом —
сверил, что bundle:sync не оставляет расхождений).
Гейты — что прогнал сам и с каким результатом
Зелёного Validate на SHA 4032b810 не найдено, поэтому прогнал сам:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
чисто |
| Unit (frontend) | npm test |
1854 pass / 0 fail / 1 skip |
| Build | npm run build |
собирается, dist создан |
| Docs fingerprint | node scripts/check-docs.mjs |
Documentation checks passed (7 files, 10 external links) |
| Backend (пул #162) | python3 -m pytest tests_backend/test_trails.py tests_backend/test_trail_recorder.py tests_backend/test_vacuum_route_validation.py tests_backend/test_vacuum_routes.py -q |
70 passed |
| Backend (полный доступный набор) | python3 -m pytest tests_backend -q --ignore=test_ha_setup.py --ignore=test_ha_upload.py --ignore=test_ha_virtual_lights.py --ignore=test_ha_websocket.py --ignore=test_ha_import_export.py |
363 passed, 8 failed — все 8 падений в test_coordinate_canonicalization.py/test_ha_support_transport.py из-за отсутствующего pytest-asyncio в этой песочнице (окружение, не #162; test_ha_* файлы у меня тоже не поднимаются — нет pytest_homeassistant_custom_component, canonical harness тут недоступен, как и задокументировано в AGENTS.md) |
| Mutation gate (точечно) | node scripts/mutation-gate.mjs --id=vacuum-calibration-solves-against-the-dock |
поймано 1 из 1 |
| Mutation gate (точечно) | node scripts/mutation-gate.mjs --id=vacuum-overlay-back-to-the-dock-space-filter |
поймано 1 из 1 |
| Смок (профильный) | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
24 прямых совпадения (список ниже), 37 слабых связей |
Смок smoke_vacuum_multifloor.mjs |
запускался автором, вывод сверен по коду — логика соответствует | не перезапускал: сценарий не пересекается с найденным дефектом (см. Находка 1) |
Смоки из «прямого совпадения», которые не гонял отдельно (автор уже прогнал
пересекающийся набор — smoke_vacuum, smoke_vacuum_firstuse,
smoke_cold_view_vacuum, smoke_danger_confirm_branches; остальные 20 —
_confirmDanger/_config/_model/_saveConfig/DevItem только по общей
инфраструктуре редактора, не по vacuum-логике — не пересекаются с изменённым
кодом по существу): не прогонял, полный набор здесь избыточен относительно
diff. smoke_editor_gestures.mjs (совпадение по _vacFit) не гонял — вместо
него написал точечный воспроизводящий скрипт (Находка 1), который прямо
отвечает на вопрос, для которого этот смок мог бы пригодиться.
Golden и performance не гонял: diff не меняет визуальный стиль puck/path (§10.1 ТЗ явно это запрещает) и не затрагивает performance-чувствительные пути сверх заявленного в АC20/пре-бета цикла; они канонически прогоняются перед бетой (AGENTS.md «Gates»), не на этом ревью.
Находка 1 (High, в скоупе) — калибровка мимо своего этажа в пути «высокий residual → ручная подгонка»
Файл: src/houseplan-editor-runtime.ts:11043-11062, метод _vacApplyCalibrationProposal.
public _vacApplyCalibrationProposal(manual: boolean): void {
const proposal = this.host._vacCalConfirm;
...
if (manual) {
const dev = this.host._devices.find((candidate) => candidate.id === proposal.markerId);
const fit = fitFromMatrix(proposal.matrix);
if (!dev || !fit) return;
this.host._markerDialog = null;
if (dev.space !== this.host._space && !this.host._commitSpace(dev.space)) return;
this.host._vacFit = {
markerId: proposal.markerId, source: proposal.source, routeId: proposal.routeId,
mapId: proposal.mapId, p: fit, drag: null,
};
return;
}
_vacAutoCalibrate (той же файл, стр. 11020-11042) правильно вычисляет
target = calibrationTarget(d.id, d.marker?.vacuum, d.space, src, mapId) и
решает матрицу против target.space (это и есть коммит 4032b810, AC8).
Когда residual выше порога, он кладёт в _vacCalConfirm только
{markerId, source, mapId, routeId: target.routeId, matrix, rooms, error} —
без space. Когда пользователь выбирает «Подогнать вручную» в диалоге
подтверждения, _vacApplyCalibrationProposal(true) заново берёт dev.space
(пространство ДОКА), а не пространство маршрута, которое было использовано
для решения матрицы. Для сравнения, независимый путь _vacStartFit
(стр. 11073-11083) делает это правильно: коммитит plan.space, посчитанный
planVacuumFit.
Воспроизведено исполнением (не только чтением): сборка + demo harness,
сценарий — робот с доком на f1 и route vr2 (m2) на garden, _space
уже равен f1 (типичная ситуация: диалог устройства открыт с этажа дока).
Симулирован ровно тот _vacCalConfirm, который кладёт _vacAutoCalibrate при
высоком residual для vr2, затем вызван _vacApplyCalibrationProposal(true):
{
"spaceBefore": "f1",
"spaceAfter": "f1",
"fitOpened": true,
"fitRouteId": "vr2",
"expectedRouteSpace": "garden"
}
_space остаётся f1, хотя fitRouteId — vr2, чья route.space —
garden. Диалог ручной подгонки открывается на этаже ДОКА и рисует матрицу,
решённую против комнат garden, поверх геометрии f1: это ровно тот
дефект, ради исправления которого создан #162 (робот показывается не на том
этаже — только внутри редактора калибровки, а не в View), и прямое
нарушение AC8 («Auto/manual calibration использует rooms/viewBox/cell size
target route.space, а не dock space»).
Почему не поймано мутант-гейтом. Заявленный мутант
vacuum-calibration-solves-against-the-dock (проверил лично —
node scripts/mutation-gate.mjs --id=vacuum-calibration-solves-against-the-dock
→ «поймано 1 из 1») патчит саму функцию calibrationTarget() и охраняется
unit-тестом на неё же. Он честно защищает то, что защищает, но не покрывает
этот конкретный стейтфул-вызов в _vacApplyCalibrationProposal, который
пересчитывает dev.space заново вместо того, чтобы использовать
calibrationTarget/сохранённый space. smoke_vacuum_firstuse.mjs
(единственный смок, гоняющий _vacApplyCalibrationProposal(true)) не ловит
это, потому что его сценарий однопространственный: dockSpace === route.space
там всегда, поэтому баг структурно не может проявиться. smoke_vacuum_multifloor.mjs
не трогает калибровку вообще, только рендер уже откалиброванных routes.
Почему в скоупе и почему это не Medium. AC8 — нормативный, явно перечисленный критерий приёмки задачи, и путь, в котором он ломается («высокий residual → ручная донастройка»), не редкий частный случай: это штатная ветка калибровки, которую спецификация специально ввела для недостаточно точного auto-calibration (§9.4, риск-таблица §19 «Recalibration портит trails»). Обходной путь пользователю недоступен без документации (диалог визуально откроется, но на неправильном этаже, что почти наверняка собьёт с толку админа, а не подскажет переключить пространство вручную).
Что нужно поправить. Сохранить space в _vacCalConfirm (типовой узел
{markerId, source, mapId, routeId?, space, matrix, rooms, error} в обоих
местах — интерфейс host._vacCalConfirm объявлен в
src/houseplan-card.ts:2511 и в HouseplanEditorHostPort
(src/houseplan-editor-runtime.ts) — и коммитить именно его в
_vacApplyCalibrationProposal, как это уже делает _vacStartFit.
Находка 2 (Medium, в скоупе) — §9.3 «Добавить источник карты» не реализован
Файл: src/editors/vacuum-maps-section.ts (весь модуль). Кнопка
«Добавить текущую карту» (addCurrent, стр. 70-81) — единственный способ
завести route; отдельного выбора camera source (§9.3 ТЗ: «переиспользует
capability picker и ленивую секцию "Все камеры"») в блоке нет.
Это прямо признано автором в комментарии реализации («Отступления от ТЗ»,
п.1) и подтверждено чтением файла — других путей создания route, кроме через
текущий root source, не существует. Проблема не гипотетическая: тело
issue #162 перечисляет это вторым обязательным сценарием дословно —
«Поддерживаются оба варианта интеграций: один источник с меняющимся map_id
и отдельная camera entity на каждую карту» — и §5.2 ТЗ включает «явный выбор
другой camera source через существующий picker» в список обязательного
scope Device editor.
Обходной путь существует (сменить root source на вторую камеру, добавить
её карту, затем при необходимости вернуть root обратно) и он функционально
работает, потому что route хранит свой source независимо от root. Поэтому
это не блокирует рендер/резолвер (AC4, AC5, AC7 проверяют resolution уже
настроенных multi-source routes и проходят), но заявленный сценарий
настройки «с нуля второй камерой, не трогая первую» не имеет прямого пути в
UI — только обходной, который нигде не объяснён пользователю.
Почему Medium, а не High: нет потери данных, нет неверного отображения; это пробел в scope UI с рабочим (хоть и не описанным) обходным путём, а не неверное поведение уже настроенного routing.
Почему в скоупе задачи, а не отдельный issue: §9.3 и часть §5.2 — это нормативный текст ТЗ, утверждённого на spec-review r2 этого же issue, и один из двух сценариев, названных в оригинальном теле #162. Это не соседнее поведение — это заявленная, но недоделанная часть текущей задачи.
Что проверено и корректно
- Резолвер и его питоновское зеркало (
vacuum-routes.ts/vacuum_routes.py) синхронны построчно:resolveRoute/resolve_route,adoptLegacyRun/adopt_legacy_run,validateMarkerRoutes/validate_marker_routes,effectiveRoutes/effective_routes— идентичная логика, идентичный порядок веток. Ambiguity — поrouteIds, не по первому совпадению (AC5, AC14, оба спот-чек мутанта поймали регрессию). - Рендер больше не фильтрует по пространству дока —
_renderVacuumsтеперь получаетthis._renderDevices(всех устройств) плюсspaceIdтекущего рисуемого пространства, и раздельно решаетlive/previousчерезplanVacuumOverlayпоroute.space, а не поd.space. Вызывается ровно один раз на кадр (единственный call site,src/houseplan-card.ts:11794), поэтому многократного пересчёта на несколько пространств одновременно нет. Мутантvacuum-overlay-back-to-the-dock-space-filter, возвращающий фильтр по док-пространству, краситsmoke_vacuum_multifloor— лично перепроверил. - Legacy-путь не переписывает конфиг сам по себе:
effectiveRoutes/effective_routesтолько читаютcalibration, ничего не пишут; явная конверсия (convertLegacyRoutes) — all-or-nothing, требует exact source (§7.3). Прочитано, логика соответствует. adoptLegacyRun/adopt_legacy_run— ровно три исхода (unique/ orphan/ambiguous), отбор поrootSourceотключается при его отсутствии, как зафиксировано в правке spec-review r1 (§11.3.1); оба зеркала идентичны.- Backend recorder (
trails.py) подписывается на объединение всех route source (не только root),_route_idнаходит совпадающий route по(source, map_id),same_run_identityразводит запуски поroute_id, если он есть, иначе — поmap_id(legacy-совместимость сохранена буквально).drop_unknown_routesвызывается изasync_purge_orphans, которая, в свою очередь, вызывается при config/set (websocket_api.py:1661) и после import — то есть удаление space/route действительно чистит связанные runs в той же логической операции (AC16), а не когда-нибудь потом. validate_marker_vacuum_routes— change-aware (не блокирует непричастный Save при уже сломанных legacy/future routes), подключена кconfig/set(оба места,websocket_api.py:1582,1941),import_export.py(полный и частичный import). Прочитано и сверено с описанием §7.2 ТЗ.space-deletion.ts— считаетrouteMarkerIds/routeCountдля чужих маркеров, ссылающихся на удаляемое пространство, и вырезает только их routes, не трогая dock/другие routes того же маркера (AC16, читал код, подтверждено).- Ratchet ядровых файлов соблюдён:
houseplan-card.ts13605/13659,houseplan-editor-runtime.ts14321/14323 (wc -l, сверено лично) — весь новый код вvacuum-routes.ts,vacuum-route-edit.ts,vacuum-maps-section.ts. - Trailers и changelog: все 11 коммитов несут
Issue: #162и корректныйUser-Visible; каждый коммит сUser-Visible: yes(c38501ef,ca6ebc30,567b4568) правит оба changelog в себе самом — сверено поgit show --stat. node scripts/check-docs.mjsзелёный на текущем дереве — отпечаток скриншотов не устарел.- RU/EN i18n parity для новых ключей
vac.route_*/vac.routes_*(лениво,i18n/support/{en,ru}.json) иvac.route_warn_*(основной словарь, обязателен для View) — точечно сверил наличие и параллельность.
Что не проверял и почему
- Полный HA harness (
test_ha_*.py, включая новыеtest_ha_import_export.py::issue_162_*) — недоступен в этой песочнице (pytest_homeassistant_custom_componentне устанавливается, канон — Linux CI/WSL, см. AGENTS.md). Автор указал то же ограничение в комментарии реализации и привёл отдельный изолированный прогон логики экспорта; сам этот отдельный прогон я не видел (не приложен как лог), но логикаimport_export.py(проверка cross-space routes, remap на импортируемый space) прочитана и не вызывает вопросов. - AC11 (reload/warm remount) — принимаю аргумент автора (маршрутизация
пересчитывается из конфига на каждый кадр, состояние между рендерами не
хранится) как правдоподобный по чтению кода:
facts.set('vacuum:<id>', …)пересобирается вrender()безусловно,resolveRoute— чистая функция от текущего конфига и снапшота состояний, ничего не кэширует между вызовами card. Backend-часть continuity покрыта существующимиtest_resume_fails_closed_on_clock_rollback_and_survives_restart_shape+ новымиtest_route_run_does_not_resume_into_another_route/test_same_route_keeps_one_run. Отдельный E2E-смок на reload/remount не считаю обязательным добавлять в рамках этого возврата — но если ревью после находки 1 потребует более широкой правки калибровки, стоит заодно зафиксировать это явным тестом, раз уж придётся трогать соседний код. - Golden и performance-профили — не гонял; вне AC этого раунда, canonical запуск — пре-бета (см. таблицу гейтов выше).
docs/images/screenshots.json— не сверял побайтово с реальными PNG, доверилсяcheck-docs.mjs(зелёный).- Остальные 20 файлов из «прямого совпадения» смок-селектора (кнопка
danger-confirm, config writer, редакторские вкладки и т.п.) — не гонял;
пересечение с изменённым кодом только по общей инфраструктуре редактора
(
_config,_saveConfig,_model,_confirmDanger), не по vacuum-логике. - 37 «слабых связей» смок-селектора — не гонял ни одного; ни одно имя
(
_model,_config,_saveConfig) не указывает на код, которого коснулся diff по существу.
Итог
Возврат автору. Обе находки — в скоупе текущей задачи и чинятся в ней же (процесс #202): отдельный issue не заводится. Находка 1 (High) требует правки перед тем, как задача может стать зелёной; находка 2 (Medium) может быть закрыта в том же коммите или отдельным, но в этой же ветке.
Материал раунда
- Ветка:
issue/162-vacuum-map-space-routing, коммит4032b810b72b— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f03a4fac82468b33357acb14bf76153213958a2agit log --all --format='%H %T' | grep f03a4fac8246 - ТЗ
docs/specs/162-vacuum-map-space-routing.md, блобa8edcfbe6c37700095f6f8582258eac4fcb58ad9git log --all --find-object=a8edcfbe6c37700095f6f8582258eac4fcb58ad9 -- docs/specs/162-vacuum-map-space-routing.md