Files
houseplan-card/docs/reviews/CODE-REVIEW-485-r2.md
T
2026-09-09 03:53:33 +03:00

320 lines
38 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-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
```