docs: review document for #485

Issue: #485
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-09 03:53:33 +03:00
committed by Matysh
parent d433b64cf0
commit f911a442c4
+319
View File
@@ -0,0 +1,319 @@
# CODE-REVIEW-485-r2
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485) — presence-radar
Stage 1. Этап: code (PROCESS.md §2.7). Заход r2, блокирующих циклов израсходовано
1 из 4 до этого раунда (r1 — красный, High 6/Medium 6).
Материал: ветка `issue/485-radar-presence`, `git log --oneline origin/dev..HEAD` /
`git diff origin/dev...HEAD`, HEAD = `1e6104100cc76195c3a05aa62e67af84793d9fd1`
("fix: keep ordinary marker dialog close synchronous"). Диапазон коммитов после
r1: `104e37b9` (lint), `cb513887`/`353a5dfa` (backend gate/tests), `c670b1bf`
("fix: harden presence radar stage one" — основная правка H1–H6/M1–M6),
`c6c94ebf` ("build: integrate radar after dev rebase"), `1e610410` (регрессия
общего закрытия диалога маркера, найденная и исправленная в той же сессии).
## Почему разбор полный, а не по дельте (PROCESS §2.10)
r1 состоялся на коммите `f9acaa35`, который умер: ветку перебазировали на ушедший
вперёд `dev` (`c6c94ebf`), и это прямо названный в процессе триггер полного
разбора («ребейз на ушедший вперёд dev — после ребейза это другой код»).
Проверено формально: `f9acaa35` не резолвится в этом дереве
(`git cat-file -t f9acaa35` → `fatal: Not a valid object name`), дерево материала
r1 `e85d8ed7d8c9…` тоже не встречается ни в одном коммите (`git log --all
--format='%H %T' | grep e85d8ed7d8c9` → пусто) — это ожидаемо после ребейза
(дерево описывает весь репозиторий, а не только диапазон задачи) и не является
находкой само по себе, как и предупреждает §2.10. Текст `dist/`/
`custom_components/houseplan/frontend/**` в `c6c94ebf` подтверждает: пересобранный
бандл получил новые хэши чанков даже для несвязанных модулей (`de-*`, `fr-*`,
`editor-*`, `houseplan-card-*`) — признак, что `dev` действительно продвинулся на
другие слитые задачи, а не просто получил тривиальный ребилд. Поэтому ниже —
полный повторный разбор backend+frontend+i18n+тесты, а не только диффа по
находкам r1; раздел «Унаследовано из r1» в конце — не поэтому пустой, а потому
что каждая находка r1 перепроверена заново с нуля.
## Как проверялось
Два параллельных фоновых агента (бэкенд: `radar.py`/`radar_validation.py`/
`radar_websocket.py`/`radar_geometry.py`+тесты; фронтенд: `radar-*.ts`/
`editors/radar-section.ts`/диффы `houseplan-editor-runtime.ts`/`houseplan-card.ts`
+тесты) прогнали весь набор findings r1 по одному, с явным «чем краснеет» на
каждый, и отдельно перечитали код целиком в поисках нового. Самые весомые их
утверждения (H3/H6/M4/новый `radar_stage1_api`/новый `bad_references`)
перепроверены мной лично чтением исходников — цитаты ниже даны по коду, который
я сам открыл, а не пересказ агента.
**Прогнанные гейты:**
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` (через `npm run bundle:sync`) | зелёный |
| `node --test test/radar-editor.test.mjs test/radar-geometry.test.mjs test/radar-model.test.mjs test/radar-setup.test.mjs test/radar-render.test.mjs` | 27/27 pass |
| `npm run build` + `cmp` трёх копий бандла | выполнено внутри `bundle:sync`, дерево синхронизировано без расхождений |
| `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (2076 строк в 14 файлах) |
| `node scripts/check-docs.mjs --screenshots=warn` | «Documentation checks passed» (strict красный по устаревшему отпечатку — ожидаемо, это гейт кандидата беты, не этого раунда, как и зафиксировал r1) |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 2 прямых совпадения на радар-символы (`smoke_radar_setup.mjs`, `smoke_radar_live.mjs`), плюс `_closeMarkerDialog`/`_markerDialog` как широкие символы из-за правки общего закрытия диалога |
| `node demo/smoke_radar_setup.mjs` | OK, все поля отчёта `true` |
| `node demo/smoke_radar_live.mjs` | OK, все поля отчёта `true` |
| `node demo/smoke_marker_stay.mjs`, `smoke_dialog_zombie.mjs`, `smoke_new_device.mjs`, `smoke_toggle_entity.mjs` (выбраны вручную сверх авто-подсказки — правка `1e610410` меняет общий путь закрытия диалога маркера для *любого* устройства, не только радара) | все 4 — OK |
| `node --test test/single-source-numbers.test.mjs` | 3/3 pass |
| backend: `python -m pytest tests_backend/test_ha_radar.py tests_backend/test_ha_radar_websocket.py tests_backend/test_radar_geometry.py tests_backend/test_radar_validation.py -q` | **не выполнен лично** — см. «Чего не проверял». Опирался на анализ гейта бэкенд-агентом (пофайловое чтение + свидетельства «чем краснеет» по каждому найденному тесту) и на подтверждённый зелёный Validate на этом же SHA (https://github.com/Matysh/houseplan-card/actions/runs/34275082664) |
| `npm run invariants` | не запускал — diff не трогает рёбра комнат/записи толщины/`layout`/`marker.space`/`open_spans` (проверено `grep` по всем изменённым backend/frontend radar-файлам, ноль совпадений); `radar.room_id` — только ссылка на существующую комнату, геометрия не меняется |
## Находки
### High — нет
Все 6 High из r1 закрыты, с конкретной проверкой каждого — таблица «Закрытие
раунда r1» ниже.
### Medium (в скоупе задачи — чинится в этой же ветке)
**M-1 (продолжение M6 из r1, закрыто частично). Три из четырёх числовых гардов
калибровочного решателя на бэкенде по-прежнему не покрыты тестом; их можно
снять незаметно.**
`custom_components/houseplan/radar_geometry.py::solve_two_point` (170–211) несёт
четыре защиты: расстояние от точки монтажа ≥50 см (177–178), допуск радиального
рассогласования (196–197), потолок RMS ≤20 см и индивидуальной ошибки ≤30 см
(199), порог неоднозначности зеркала <10 см (209–210). Из них
`tests_backend/test_radar_geometry.py::test_two_point_fit_is_rigid_and_rejects_collinear_references`
(73–82) проверяет только коллинеарность (угол 0°). Ни один backend-тест не
строит референс ближе 50 см к монтажу, не создаёт измеримую RMS-ошибку и не
создаёт двух близких по RMS зеркальных кандидатов — удаление любой из этих трёх
защит (`radar_geometry.py:177-178`, `:199`, `:209-210`) не уронит ни один
backend-тест. Симметричная TS-реализация (`src/radar-geometry.ts`,
`test/radar-geometry.test.mjs:40-59`) все четыре гарда покрывает — но это
отдельная реализация, не тот же код-путь, и не защищает Python-версию.
Отличие от исходного M6: 2 из 8 отсутствовавших кодов ошибок
(`unsupported_capability` — `radar_websocket.py:69-78`, тест
`test_ha_radar_websocket.py:120-128`; `source_unavailable` —
`radar_websocket.py:59-65`, тест `test_ha_radar_websocket.py:205-220`) теперь
реализованы и протестированы полностью — эта часть M6 закрыта.
*Проверка:* `python -m pytest tests_backend/test_radar_geometry.py -q -k solve_two_point` — единственный релевантный тест; сравнить с 4-кратным покрытием того же набора гардов в `test/radar-geometry.test.mjs`.
**M-2 (новая). `settings.radar_stage1_api` объявляется константой `1` независимо
от состояния координатора — вопреки явному тексту спецификации.**
`custom_components/houseplan/websocket_api.py:1409` — `"radar_stage1_api": 1`
жёстко закодировано в ответе `ws_config_get`, без проверки
`rt.radar_coordinator`. Спецификация (`docs/specs/485-radar-presence-stage1.md:495-496`,
§7): «`config/get` advertises `radar_stage1_api:1` **only after coordinator
setup**. Absent capability disables radar operations with an update message;
fields still survive.» Проверено чтением `custom_components/houseplan/__init__.py`:
`data.radar_coordinator = RadarCoordinator(hass, data)` затем
`await data.radar_coordinator.async_setup()` без `try/except` (61–88) — при
ошибке `async_setup()` `async_setup_entry` целиком падает; при выгрузке записи
(`__init__.py:299-301`) `radar_coordinator` становится `None`, но уже сохранённый
`entry.runtime_data` может пережить это окно, если клиент успевает вызвать
`config/get` между `teardown()` и повторной установкой при перезагрузке записи.
В этом окне фронтенд получит `radar_stage1_api: 1`, попробует
`houseplan/radar/subscribe` и получит непредсказанную ошибку вместо
задокументированного «graceful degrade» с update-сообщением. Ни один backend
или frontend тест не проверяет значение этого поля (`grep radar_stage1_api` по
`tests_backend/` — пусто); фронтенд (`src/version-recovery-card.ts:66`) уже готов
корректно обработать отсутствие капабилити (`=== 1 ? 1 : null`) — не хватает
только серверной стороны условия.
**M-3 (новая). `radar.bad_references` — объявленный и переведённый текст, на
который код никогда не попадает; неоднозначная и «недалеко от монтажа»/
коллинеарная ошибки выбора точек показывают общий, менее полезный текст.**
`src/radar-geometry.ts:44-53`: и гард «референс ближе 50 см от монтажа», и гард
«угол между референсами <20°/>160°» (буквально совпадающий с текстом
`bad_references`: «Choose separated references in different directions from the
sensor») бросают одно и то же `invalid_selection`; провал radial/RMS-подбора
(строка 77, `if (!candidates.length) throw new Error('invalid_selection')`)
бросает то же самое. `src/radar-setup.ts:333`:
`state.error = message === 'ambiguous_sources' ? 'radar.ambiguous_sources' :
'radar.bad_fit'` — то есть *всё*, что не «ambiguous», получает
`radar.bad_fit` («Measurements do not match. Check units, mounting point and
references»), а специально переведённый на 4 языка (`src/i18n/en.json:494`)
`radar.bad_references` («Choose separated references in different directions
from the sensor») не читается никаким `.ts`-файлом вообще (проверено `grep -r
bad_references src/ test/` — только объявление в словарях). Сценарий: пользователь
ставит обе контрольные точки почти на одной прямой от датчика (частая ошибка —
обе точки у одной стены) и получает общее «проверьте единицы/установку», а не
конкретную подсказку «выберите более разнесённые направления», которую именно
для этого случая спроектировала и перевела спецификация. Функционально мастер
работает (ошибка показывается, повторный ввод возможен), это не блокирует
сценарий, но выданное указание вводит в заблуждение ровно как M4 из r1 (тот же
класс дефекта, другая пара сообщений). Не поймано существующим гейтом
`test/i18n-dead-keys.test.mjs`, потому что его паттерн для динамических ключей
слишком широк (детали — issue #502 ниже, вне скоупа этой ветки).
*Проверка:* `grep -n "bad_references" src/*.ts test/*.mjs` — совпадений в коде нет, только в `src/i18n/*.json`.
### Low (на усмотрение автора, не блокируют)
- Фикс H4 (комната без контура открывает мастер с предупреждением) корректен по
чтению (`src/editors/radar-section.ts:185-197`, ключ `radar.no_contour`
переведён отдельно и по-разному на все 4 языка), но не покрыт ни одним тестом:
ни `test/radar-editor.test.mjs`/`radar-setup.test.mjs`, ни
`demo/smoke_radar_setup.mjs` не строят комнату с `poly.length < 3` и не
проверяют, что мастер всё равно открывается (а не рефьюзится, как раньше).
Удаление этой ветки кода не уронит ни одного текущего теста.
- Основной механизм M3 («Изменить установку» получает свежий UUID даже при
совпадающих итоговых координатах) верен по трассировке кода
(`src/editors/radar-section.ts:198-204` → `radarConfigFromDraft` →
`mount.installation_id`), но именно этот сценарий (совпадающие x/y, новый UUID)
не проверен тестом — единственная существующая проверка UUID
(`test/radar-editor.test.mjs:69`) доказывает лишь, что генератор не
детерминирован, а не то, что кнопка действительно его вызывает.
- Планка 4 кадра/с (`custom_components/houseplan/radar.py:236-256`,
`_schedule_publish`/`_flush_pending`) — общая на весь координатор
(`_pending`/`_last_publish_monotonic` — поля координатора, не радара), тогда
как §4 спецификации формулирует её как «4 geometry frames/s/radar». При
нескольких одновременно активных радарах (до 32 по лимиту) их суммарная частота
делится на общий бюджет, а не даётся каждому независимо — это не превышение
потолка (не гонка/утечка), а более скромная деградация под нагрузкой, чем
буквально обещано текстом.
- Ни для одного из AC S1-1/S1-5/S1-13/S1-16/S1-19 нет golden-сцены: `find demo/golden
-iname "*radar*"` — пусто, ровно то же наблюдение, что r1 сделал в разделе «Как
проверялось», не переросшее в Medium. За прошедший раунд это не изменилось.
Не поднимаю до Medium в этом раунде тоже: там, где AC называет golden
соевидетелем наряду с unit/smoke, конкретные защитные утверждения (частичное
здоровье, отрисовка зон, гейт допустимости, отказ неподдерживаемых команд) уже
доказаны прицельными негативными тестами с честным «чем краснеет» — см. таблицу
ниже. Рекомендую добавить радарную golden-сцену до кандидата беты, не как
условие зелёного вердикта этого раунда.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| H1 — «partial» не существует, смешанные/полностью устаревшие данные неотличимы | `radar.py:551-554` вычисляет `has_current_evidence`, различает `partial` (часть слотов свежая, часть нет) и `stale` (нет ни одной свежей) как разные ветки | `tests_backend/test_ha_radar.py::test_mixed_current_and_stale_coordinate_slots_report_partial` (108-129) строит 2-слотовый радар с 1 свежим+1 просроченным слотом, `occupancy=True`, и требует `health == "partial"`, `complete is False` — схлопывание в `stale` уронило бы тест |
| H2 — гонка teardown/await воскрешает подписки | `async_refresh()` (121-148) берёт `self._lock` и перепроверяет `self.closed` сразу после единственного await; `teardown()` (150-172) выставляет `closed=True` синхронно до `async_track_*` cleanup | `test_teardown_during_config_load_cannot_resurrect_subscriptions` (307-324) — реальный неразрешённый `asyncio.Future` как результат `config_store.async_load`, `async_setup()` запущен как задача, `sleep(0)` даёт ей засеять на await под локом, `teardown()` вызывается пока задача приостановлена, затем future резолвится — тест требует `radars == {}`, `_unsub_config is None`, `_unsub_sources == []`; это настоящее чередование await, не последовательный вызов |
| H3 — нет same-device проверки для LD2450 | `radar_validation.py:147-180` (`_validate_verified_adapter`) требует данные реестра, сверяет `device_id` всех источников (ровно одно устройство), `platform == "esphome"`, модель в `LD2450_MODELS`, и отдельно — совпадение с устройством самой привязки маркера | `test_radar_validation.py::test_ld2450_verified_adapter_requires_same_esphome_marker_device` (69-88) — 4 разных отказа (чужое устройство/чужая модель/чужое устройство привязки) с разными сообщениями; воспроизведено также в `test_ha_radar.py` через `_coordinator()` с реальными записями реестра |
| H4 — отказ вместо предупреждения для комнаты без контура | `src/editors/radar-section.ts:185-197` теперь отдельно обрабатывает «нет комнаты вовсе» (отказ, `radar.invalid`) и «комната есть, контур короче 3 точек» (предупреждение `radar.no_contour`, мастер всё равно открывается — `setup.begin` не проверяет `room.poly`) | Прочитано и прослежено по коду `configureOnPlan`/`RadarSetupController.begin` (`src/radar-setup.ts:108-119`); ключ `radar.no_contour` переведён на 4 языка. **Автотеста нет** — см. Low выше |
| H5 — известные зоны не рисуются | `src/radar-render.ts:18-24` рендерит `frame.zones`, фильтруя занятые, в `<polygon class="radar-zone">`; CSS `src/styles/plan.styles.ts:1448-1454` — `stroke-width: 2px`, `fill: color-mix(…8%, transparent)` | `test/radar-render.test.mjs:11-20` — рендерит занятую и пустую зону, проверяет класс/id/точки полигона и что пустая зона даёт `nothing` |
| H6 — смена привязки может стереть сохранённую конфигурацию | `src/radar-editor.ts:65-75` (`radarAfterBindingChange`) различает черновик с `.original` (сохранённый — переживает смену привязки нетронутым) и без него (несохранённое Q5-объявление — стирается); оба места вызова в `houseplan-editor-runtime.ts` (13015, 13071) используют эту функцию вместо `radarRemove: d.radarRemove \|\| !!d.radar` | `test/radar-editor.test.mjs:60-70` — сохранённый черновик даёт `{radar:saved, radarRemove:false}`, несохранённый — `{radar:null, radarRemove:true}` |
| M1 — Stage 2/3 валидация реализована преждевременно | `_validate_stage2` удалена полностью (её больше нет в файле); `_validate_settings` (358-363) проверяет только `version`/`show_live` | `test_future_stage_extensions_are_preserved_but_inert_in_stage1` (140-148) — блок `zones.local` с геометрически невалидным (вырожденным) полигоном проходит нетронутым; `test_future_fusion_and_output_settings_round_trip_without_stage1_validation` (227-248) — то же для `fusion_groups`/`room_outputs` |
| M2 — распознавание по тексту manufacturer/model | `src/radar-editor.ts:55-127` требует структурное совпадение: `canonicalModel(...)` ∈ curated `LD2450_MODELS`, плюс суффиксное совпадение entity id (`target_1_x`/`target_1_y`); поле `manufacturer` объявлено в интерфейсе, но в логике сопоставления не читается | `test/radar-editor.test.mjs:49-58` — устройство с `model: 'Presence Radar FP2'` и координатными именами сущностей **не** признаётся допустимым (негативный тест) |
| M3 — «Изменить установку» не реализовано; закрытие «грязного» мастера не спрашивает | `changeInstallation()` (`radar-section.ts:198-204`) генерирует свежий `installationId`, доступна только для `radar.original` (сохранённого); `RadarSetupController.discardIfAllowed()` (`radar-setup.ts:130-137`) спрашивает подтверждение через `host.confirmDiscard()`, `_closeMarkerDialog` маршрутизирует через guarded-путь при `isDirty()` | `test/radar-setup.test.mjs:46-80` — отмена на чистом/грязном+подтверждённом/грязном+отклонённом состоянии, все три ветки. Сценарий «одинаковые x/y → новый UUID» именно через кнопку — не тестирован, отмечено в Low |
| M4 — неоднозначность маскируется под «плохие референсы» | `src/radar-setup.ts:331-336` — `ambiguous_sources` маппится в отдельный `radar.ambiguous_sources`, отличный от `radar.bad_fit` | `test/radar-geometry.test.mjs:54-59` — RMS двух зеркальных гипотез <10см даёт именно `ambiguous_sources`; словари 4 языков дают разный текст для двух ключей |
| M5 — блок радара не в конце тела диалога | Основной (уже настроенный/распознанный) раздел остался на прежнем месте намеренно (спецификация требует «в конце» только для Q5-объявления неопознанного устройства); Q5-действие (`placement === 'additional'`, `radar-section.ts:76-86`) теперь рендерится в `<details class="radar-additional">` после Model/Link/Description/PDF и перед `footer` (`houseplan-editor-runtime.ts:13573`, до `:13575`) | Прочитано по месту вызова: секция вставлена между блоком PDF (13542-13572) и `<div class="row marker-footer" slot="footer">` (13575) |
| M6 — 2/8 кодов ошибок отсутствуют; гварды решателя не тестированы | Коды `unsupported_capability`/`source_unavailable` реализованы и протестированы (см. M-1 выше). Гварды решателя закрыты **частично**: коллинеарность — тестирована, дистанция/RMS/неоднозначность на Python-стороне — нет | См. новую находку **M-1** выше — перенесено туда как незакрытый остаток, не задвоено в оба списка |
| Low: USER-GUIDE цитирует несуществующий текст переключателя | `docs/USER-GUIDE.md:739`/`ru.md` теперь цитируют «Show live presence on the plan» / «Показывать текущее присутствие на плане», совпадает с `gs.radar_show_live` в `en.json:468`/`ru.json:468` дословно | `grep` подтверждает совпадение |
| Low: `restricted`/`incomplete` схлопываются в `health_unknown` | `RADAR_HEALTH_KEYS` (`radar-model.ts:16-22`) получил отдельные `radar.health_restricted`/`radar.health_incomplete` | Прочитано по коду |
| Low: `report_age` не показывается в Advanced | `radar-section.ts:461-464` вычисляет и показывает возраст отчёта | Прочитано по коду |
| Low: `recognizeRadar` не в `radar-model.ts` | Не перенесено — предикат остался в `radar-editor.ts` | Архитектурная рекомендация, не обязательная; автор не обязан был чинить Low |
## Что проверено и корректно (сверх таблицы закрытия r1)
- **Права и лимиты не ослаблены при полном перечитывании**: `may_write` на
`setup/inspect`/установку, per-entity ACL на подписку/инспекцию, лимиты
10 инспекций/мин, 4 live-подписки/пользователь, 8 глобальных/1 на маркер
setup-подписок и `MAX_RADARS=32` — все на месте с тестами (подтверждено
фоновым агентом и выборочно перепроверено чтением `radar.py`/`radar_websocket.py`).
- **Регрессия общего закрытия диалога маркера (`1e610410`) устранена корректно**:
до фикса `_closeMarkerDialog()` стал безусловно асинхронным
(`void this._closeMarkerDialogGuarded()`) из-за проверки радар-состояния,
затрагивая *все* маркеры, не только радарные. Сейчас — синхронное закрытие,
если `_radarSetup.isDirty() === false` (обычный случай), и guarded-путь только
при незавершённой калибровке. `test/radar-setup.test.mjs` получил прямые
проверки `isDirty()` на пустом/начатом состоянии; смоки `smoke_marker_stay`,
`smoke_dialog_zombie`, `smoke_new_device`, `smoke_toggle_entity` (выбраны вручную
как нерадарные потребители общего пути закрытия) — все зелёные.
- **Один источник числа**: живая координата в «Advanced» диагностике и координата,
используемая для проекции/принадлежности зоне, идут из одного и того же
`RadarLiveFrame`/`radar-model.ts`; клиентская геометрия (`projectRadarLocal`)
используется только внутри эфемерного, сессионного мастера настройки, не в
принятом (post-Save) пути отображения. Регрессии класса #234/#233 не
просматривается.
- **Новый `any` не добавлен** (`no-new-any.mjs`, 2076 строк/14 файлов — 0 совпадений).
- **i18n-структура**: `en`/`ru`/`de`/`fr` синхронно получили одинаковый набор
новых ключей (`git diff --stat` — по 106 строк в каждом словаре); `test/i18n.test.mjs`
разрешает намеренные англо-идентичные `de`/`fr` значения (`radar.source_optional`,
`radar.heading`, `radar.radians`) явным списком, а не молчаливым допуском.
- **Трейлеры и changelog**: оба `User-Visible: yes` коммита диапазона (`5e3adac4`,
`c670b1bf`) несут правки в `docs/CHANGELOG.md`+`docs/CHANGELOG.ru.md` в себе же;
`Issue: #485` — во всех коммитах диапазона.
- **Инварианты геометрии не применимы**: radar-код не трогает рёбра/толщину/
`layout`/`open_spans` — проверено `grep` по всем изменённым файлам.
## Чего не проверял
- **`python -m pytest tests_backend/test_ha_radar*.py test_radar_geometry.py
test_radar_validation.py -q` лично не прогонял.** Песочница ревью не имеет
готового venv Python 3.14 с `pytest-homeassistant-custom-component`; попытка
создать venv (`/opt/az/bin/python3.14 -m venv` + `pip install -r
tests_backend/requirements.txt`) прошла успешно, но сам pytest упал на
`ModuleNotFoundError: No module named '_sqlite3'` — системный Python 3.14 в этом
окружении собран без `sqlite3` (нет `_sqlite3.so`), а
`pytest_homeassistant_custom_component.plugins` жёстко импортирует `sqlite3`.
Чинить это через пересборку Python или системный `apt` ради одного раунда
ревью — непропорциональные действия. Опираюсь на: (а) зелёный Validate на
точном SHA `1e610410` (backend job гоняет полный `tests_backend`,
https://github.com/Matysh/houseplan-card/actions/runs/34275082664); (б)
построчный разбор каждого нового/изменённого теста фоновым агентом с указанием
точной команды и того, что именно мутирует находка — эквивалент «прочитано, не
исполнено» для каждого конкретного теста, включая свежепостроенный
race-тест H2, который я прочитал лично и подтверждаю, что он действительно
чередует await, а не вызывает методы последовательно.
- **`npm run golden:verify` не прогонял в этом раунде.** r1 уже установил (свой
контрольный прогон на чистом `origin/dev` тем же набором «different» сцен), что
расхождения полного матрикса — окруженческий шум этой песочницы, не регрессия
ветки; diff с r1 по golden-релевантным файлам — только уже принятый
`general-color-popover-desktop-en` baseline (не тронут дальше) и
`baselines-index.json`. Правок в рендер, задевающих существующие golden-сцены,
в дельте r1→r2 нет (единственная новая визуальная поверхность — `radar-render.ts`/
CSS, для которой golden-сцен не существует вовсе — см. Low).
- **Полный `demo/smoke_*.mjs` матрикс (234 файла)** не гонял — `smoke-select.mjs`
вернул только 2 прямых совпадения (оба зелёные) плюс широкие символы
(`_closeMarkerDialog`/`_markerDialog`/`_editorRuntime`/`_mode`/`_cfgRev`),
закономерно задетые правкой общего диалога; вместо полного прогона выбрал 4
дополнительных нерадарных смока, реально использующих `_closeMarkerDialog`
(обоснование выбора — в таблице гейтов). Полный матрикс — предрелизная
обязанность (PROCESS §8), не обязанность этого раунда.
- **`python -m pytest tests_backend -q` (весь набор, 799 тестов)** — не гонял по
той же причине окружения; полагаюсь на зелёный Validate тем же аргументом, что
и r1 использовал для этого пункта.
- **Perf-профили** — не запускал: diff не трогает `src/iso-*`, `src/live-*`,
`src/render-*`, `houseplan-render-lifecycle.ts` управляющие профилями пути
напрямую помимо уже покрытых двумя всегда-идущими glow-профилями в Validate;
AC этой стадии не называют performance-профиль отдельным доказательством.
## Итог
0 High. 3 Medium в скоупе (M-1 продолжение M6, M-2 капабилити `radar_stage1_api`,
M-3 мёртвый `radar.bad_references`) — все три доказаны конкретным чтением кода и
названной командой/сценарием, все три чинятся в этой же ветке без переработки
архитектуры. Отдельно заведён issue [#502](https://github.com/Matysh/houseplan-card/issues/502)
на найденный вне скоупа этой ветки, но обнаруженный при её разборе, общий дефект
теста `i18n-dead-keys` (широкий паттерн `^r.+$` маскирует мёртвые i18n-ключи по
всему проекту, не только радарные) — это отдельная задача с меткой `tech-debt`,
не правится внутри #485. 6 High и всё содержательное из 6 Medium r1 закрыто
предметно, с воспроизводимой проверкой каждого (таблица выше); часть Low r1 тоже
закрыта, не будучи обязательной. Вердикт — жёлтый: без High это возврат автору на
доработку тех же трёх Medium в текущем issue, отдельный цикл не по архитектуре, а
по трём точечным правкам (backend-тесты на 3 гварда, условие капабилити по
`radar_coordinator`, маршрутизация `invalid_selection`→`bad_references` отдельно
от radial/RMS-провала).
---
<!-- material-anchors: заполняется конвейером публикации -->
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/485-radar-presence`, коммит `1e6104100cc7` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `da6ff25c3f681ed1584526e941b2dbc47e476705`
```
git log --all --format='%H %T' | grep da6ff25c3f68
```
- ТЗ `docs/specs/485-radar-presence-stage1.md`, блоб `f6e7e122303a4c00192be58cdc387bbc4859c1e6`
```
git log --all --find-object=f6e7e122303a4c00192be58cdc387bbc4859c1e6 -- docs/specs/485-radar-presence-stage1.md
```
- ТЗ `docs/specs/485-radar-presence-stage2.md`, блоб `b403e73fc26ba0ffb97be4ffa73be067d179880f`
```
git log --all --find-object=b403e73fc26ba0ffb97be4ffa73be067d179880f -- docs/specs/485-radar-presence-stage2.md
```
- ТЗ `docs/specs/485-radar-presence-stage3.md`, блоб `c65e28389d762216c47f00249bdb751f3c9d5082`
```
git log --all --find-object=c65e28389d762216c47f00249bdb751f3c9d5082 -- docs/specs/485-radar-presence-stage3.md
```
- ТЗ `docs/specs/485-radar-presence.md`, блоб `3297ed0140265ad86f2072a40cb9bf8097c0674d`
```
git log --all --find-object=3297ed0140265ad86f2072a40cb9bf8097c0674d -- docs/specs/485-radar-presence.md
```