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

38 KiB
Raw Permalink Blame History

CODE-REVIEW-485-r2

Issue: #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 на найденный вне скоупа этой ветки, но обнаруженный при её разборе, общий дефект теста 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-провала).



Материал раунда

  • Ветка: 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