Files
houseplan-card/docs/reviews/CODE-REVIEW-485-r1.md
T
2026-09-09 03:52:22 +03:00

409 lines
35 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-r1
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485) — presence-radar
Stage 1. Этап: code (PROCESS.md §2.7). Заход r1, блокирующих циклов израсходовано
0 из 4 до этого раунда.
Материал: ветка `issue/485-radar-presence`, `git log --oneline origin/dev..HEAD`
/ `git diff origin/dev...HEAD`, HEAD = `f9acaa35` ("build: integrate radar with
current dev"). Диапазон коммитов: `5e1d7f03..f9acaa35` (spec-review документы +
`578df257` feat, `8e03dec7` chore, `4aacf8a4`/`5b975310`/`032435dd`/`5019eae5`
fix/test, `f9acaa35` build). Диапазон затрагивает 113 файлов, +10832/-732 строк.
Реализационная authority — `docs/specs/485-radar-presence-stage1.md` (проверена
целиком; общий контракт `485-radar-presence.md` использован для терминологии
Stage 2/3-полей).
## Скоуп
Первый раунд — предыдущих код-ревью нет, разбор полный. Раздел «Унаследовано»
не пишу — наследовать нечего.
Проверено: полный бэкенд (`radar.py`, `radar_geometry.py`, `radar_validation.py`,
`radar_websocket.py`, диффы `__init__.py`/`store.py`/`validation.py`/
`websocket_api.py`/`import_export.py`), весь новый фронтенд (`radar-model.ts`,
`radar-geometry.ts`, `radar-live.ts`, `radar-render.ts`, `radar-setup.ts`,
`radar-editor.ts`, `editors/radar-section.ts`, дифф `houseplan-editor-runtime.ts`
/`houseplan-card.ts`/`types.ts`/стили), i18n (en/ru/de/fr), все новые тесты и
смоки, `docs/RADAR.md`, дифф `USER-GUIDE.{md,ru.md}`, `docs/SCOPE.md`, changelog,
config-schema/field-registry.
## Как проверялось
Разбор вели три параллельных агента (бэкенд-код, фронтенд-код,
тест-качество+доки) с конкретными пунктами спецификации для сверки построчно;
самые серьёзные и неожиданные находки перепроверены мной лично чтением
исходников (см. ниже) — «доверяй, но проверяй» применён к каждому High.
**Прогнанные гейты и почему:**
- `npx tsc --noEmit`, `npm test`, `npm run build` (со сверкой бандла) —
**не перегонял**: Validate на `f9acaa35` зелёный
(https://github.com/Matysh/houseplan-card/actions/runs/34262041971), это
дешёвые гейты из уже подтверждённого прогона. Инцидентально `npm run build`
всё же выполнялся мной (см. ниже про golden) и прошёл чисто, что попутно ещё
раз подтверждает tsc.
- `node --test test/radar-editor.test.mjs test/radar-geometry.test.mjs
test/radar-model.test.mjs test/radar-setup.test.mjs` — прогнал (через
агента): **22/22 pass**.
- `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` (Python 3.14 venv, т.к. в песочнице
по умолчанию 3.12, а `pytest-homeassistant-custom-component==0.13.357`
требует ≥3.14) — прогнал (через агента): **81/81 pass**; дополнительно
`pytest tests_backend/ -q --collect-only` — 799 тестов, ошибок импорта нет.
Полный прогон всех 799 backend-тестов не проводил: это уже покрыто зелёным
Validate на этом же SHA (backend job гоняет полный `tests_backend`), а
collect-only исключает поломку импортов от новых модулей.
- `node demo/smoke_radar_setup.mjs` и `node demo/smoke_radar_live.mjs` — прогнал
лично: оба **OK** (все булевы поля отчёта `true`).
- `npm run golden:verify` — прогнал лично (после `npm run bundle:sync`, без
которого стенд `demo/srv/assets` не синхронизирован и тест падает на
`Failed to fetch dynamically imported module` — это issue-независимая ловушка
окружения, не находка). Результат: **119 из 150 сцен «different»**. Поставил
контрольный прогон той же командой на чистом `origin/dev` (git worktree,
`586494b7`) — тот же набор сцен «different» с теми же именами в том же
порядке. Это подтверждает: расхождения — окруженческий шум (рендер/шрифты
этой песочницы против CI-снятых эталонов), не регрессия этой ветки. Единственная
осмысленная находка из golden — не сами расхождения, а их отсутствие для
радара: **в `demo/golden/baselines/` нет ни одной сцены со словом «radar»**
(проверено `find`/`grep`) — при том что S1-1 и S1-19 называют
«en/ru light/dark golden» и «device-editor smoke/golden for coordinate/range/
zone/presence radars... light/switch/temperature/PIR/virtual/unknown devices
and saved broken/future config» ровно доказательством по этим AC. Обещанный
способ доказательства отсутствует целиком, не частично.
- `node scripts/check-docs.mjs` — по умолчанию (strict) падает: «screenshot
source fingerprint is stale». Это ожидаемо и не находка: `scripts/
docs-freshness.mjs` документирует, что strict — это режим кандидата беты и
релизного гейта, а обычный пуш использует `--screenshots=warn`; с `warn`
проверка проходит чисто (7 файлов, 12 внешних ссылок). Раз диффа `src/**`
тут неизбежно делает отпечаток «устаревшим», и обновление отпечатка — задача
отдельного `docs: refresh screenshot fingerprint` коммита перед бетой, а не
этого раунда.
- `npm run invariants` — **не запускал**: diff не трогает рёбра комнат, записи
толщины, `layout`, `marker.space`, `open_spans` (проверено `grep` по всему
дифф-списку файлов и по тексту radar*.py/radar-*.ts — совпадений нет).
`radar.room_id` — ссылка на существующую комнату, но геометрия комнаты не
меняется и не создаётся радаром.
- Полный `demo/smoke_*.mjs` матрикс — не гонял. `node scripts/smoke-select.mjs
--base origin/dev --head HEAD` вернул 2 «прямых совпадения» именно на
радар-символы (`smoke_radar_setup.mjs ← _radarSetup`, `smoke_radar_live.mjs
← _haRadarStage1Api,_radarLive,_syncRadarLive`) — оба прогнаны выше и
зелёные. Остальные ~53 «прямых» и ~52 «слабых» совпадения инструмент относит
к широким символам (`_editorRuntime`, `_markerDialog`, `_mode`, `_cfgRev`,
`cellCm`), закономерно задетым любой правкой в общем диалоге маркера —
порог «широкого» символа инструмент сам поднял до 46 смоков. Конкретные
находки ниже получены чтением кода, а не через смоки; прогон полной матрицы
не добавил бы уверенности сверх уже найденного и является предрелизной
обязанностью (PROCESS §8), не обязанностью этого раунда.
## Находки
### High (блокируют)
**H1 — «partial» не существует как состояние здоровья; смешанные и полностью
устаревшие данные неотличимы.**
`custom_components/houseplan/radar.py`, сборка `frame["health"]` (~540–543):
`elif any_stale: frame["health"] = "stale"` срабатывает и когда устарел один
слот из трёх, и когда устарели все. Конкретный сценарий: 3-слотовый
`cartesian_v1` радар, один слот — свежая валидная цель, два — просрочены,
occupancy=True. `frame["targets"]` не пуст → ветка `position_unavailable`
пропущена → `health="stale"`, неотличимо от радара, где абсолютно все данные
устарели. Спецификация (§9, AC S1-5) требует, чтобы `off/unknown/stale/
partial/inconsistent/occupied-without-position` были «genuinely distinct
states in code, not collapsed» — это прямое нарушение центрального для всей
задачи принципа честности статуса измерения (§4). Ни один текущий тест не
покрывает смешанный много-слотовый сценарий.
**H2 — гонка teardown/await воскрешает HA-подписки после выгрузки интеграции.**
`custom_components/houseplan/radar.py`, `async_setup()`/`async_refresh()`
(104–139). `async_refresh()` проверяет `self.closed` один раз **до**
`await self.runtime.config_store.async_load()` и не перепроверяет после
возврата из await; `teardown()` (141+) — синхронный метод, не берёт
`self._lock`. Сценарий: `houseplan_config_updated` планирует
`async_create_task(self.async_refresh())`; задача приостанавливается на
await; параллельно интеграция выгружается — `teardown()` отрабатывает
полностью (`self.closed=True`, `_unsub_sources` очищен); задача возобновляется
и без повторной проверки `self.closed` вызывает `self._resubscribe_sources()`
(регистрирует новые `async_track_state_report_event`/
`async_track_state_change_event`) и `self._publish_all(force=True)`. Эти
новые HA-слушатели никогда не будут сняты — объект-координатор уже выброшен
`async_unload_entry` (`__init__.py:299-301`), никакой будущий `teardown()` до
них не доберётся. Это ровно сценарий, который спецификация запрещает явно
(§7: «Teardown checks after awaits prevent resurrection on unload», AC S1-6).
Тот же паттерн — во втором await внутри `async_setup()`.
**H3 — нет проверки «то же устройство» для верифицированного адаптера LD2450.**
`custom_components/houseplan/radar_validation.py` нигде не читает
`device_id`/реестр устройств (grep по файлу и по `radar.py` — ноль совпадений);
сигнатуры валидаторов не принимают `hass`/снимок реестра. Сценарий: черновик
`{profile: "esphome_ld2450_v1", unit: "mm", x_entity: "sensor.kitchen_temperature",
y_entity: "sensor.unrelated_integration_humidity"}` — два датчика с разных
устройств/интеграций — проходит `_validate_sources` и принимается как
«верифицированный ESPHome LD2450», включая специфичную только для этого
адаптера семантику «X=0,Y=0 = явное отсутствие» (`radar.py:586-587`), на
которую он права не имеет. Прямое нарушение §3 («require the same ESPHome
device») и §7 («Same-device checks remain mandatory for verified hardware
adapter roles, not generic input»).
**H4 — «Настроить на плане» отказывает для комнаты без контура вместо
предупреждения и незанятой настройки.**
`src/editors/radar-section.ts:175-185`:
```js
if (!space || !room || !Array.isArray(room.poly) || room.poly.length < 3
|| !radarConfigFromDraft(radar, space.cellCm || 5)) {
options.toast(options.t('radar.invalid'));
return;
}
```
Комната «есть, но без контура» (`poly.length < 3`) обрабатывается идентично
«комнаты нет вовсе» — мастер настройки вообще не открывается, только общий
toast об ошибке. Спецификация требует обратного: «A room without contour
warns "No room boundary: points are not clipped"» (§5.2, шаг 1) и «existing
room with no usable contour allows unclipped calibrated output with the
stated warning» (§5.3, AC S1-11: «missing contour warns, missing room never
retargets»). Подтверждено также, что строки `radar.no_contour` нет ни в одном
из четырёх словарей i18n — предупреждение не реализовано вообще, не только
не показано в этом месте. Целый класс валидных Stage‑1 установок (радар,
смотрящий в комнату без нарисованного контура) недостижим через UI.
**H5 — известные занятые зоны (`zones_v1`) никогда не рисуются на плане.**
`grep -n "zone" src/radar-render.ts` — ноль совпадений; `renderRadarLive`
обрабатывает только `frame.ranges` и `frame.targets`. `RadarLiveFrame.zones`
нормализуется (`radar-model.ts:96-103`) и профиль `zones_v1` полностью
конфигурируется в редакторе (`radar-editor.ts`, `editors/radar-section.ts`),
но результат нигде не отображается. AC S1-13 требует «2 px outline и
≤0.08 fill opacity» для известных занятых зон — реализации нет: класса
`.radar-zone`, заливки или контура не существует. Пользователь, настроивший
zones_v1-радар, не увидит на плане вообще ничего — профиль нефункционален
целиком, не частично.
**H6 — смена HA-привязки маркера может молча удалить уже сохранённую,
откалиброванную конфигурацию радара.**
`src/houseplan-editor-runtime.ts` (два места смены привязки, ориентировочно
~13019 и ~13077): `radarRemove: d.radarRemove || !!d.radar` срабатывает при
любом истинном `d.radar` — включая случай, когда это существующий сохранённый
`marker.radar` (загружен через `radarDraft` с `reason:'saved'`), а не только
несохранённое ручное объявление Q5. При следующем Save (`_saveMarker`,
`radarField = { radar: null }`) ранее откалиброванная конфигурация стирается
без предупреждения и без возможности восстановления — это отличается от
явного требования спецификации для аналогичного случая смены пространства
(§6: «preserves original block inert as needing setup, never projects it onto
another floor»). Смена привязки радара сегодня — не «неактуально до ремонта»,
а безвозвратная потеря настройки.
### Medium (в скоупе задачи — чинится в этой же ветке)
**M1 — в «Stage 1» реализована валидация Stage 2/3.**
`custom_components/houseplan/radar_validation.py`: `_validate_stage2` (232-289)
полностью валидирует `radar.zones.local` (полигоны, кворум состояния),
`radar.zones.hardware` (`esphome_ld2450_numbers_v1`, упорядоченные слоты
number-сущностей) и `radar.reflectors`; `_validate_settings` (376+) валидирует
`settings.radar.fusion_groups`/`room_outputs` (уникальность групп, состав
2..8 маркеров, эксклюзивность членства). Типы объявлены в `src/types.ts:
154-155,317-318`; тесты `tests_backend/test_radar_validation.py` прямо названы
`test_stage2_*`/`test_valid_fusion_and_room_output_settings`. Согласно
`docs/specs/485-radar-presence-stage1.md` (шапка документа: «Stages 2 and 3
remain separate, unimplemented scopes») и §1 («Excluded: … zone writes/
drawing, reflectors, multi-room fusion, heat maps and derived HA entities
(specified in stages 2–3)») этого быть не должно. §6 разрешает лишь «Known-only
field validation does not strip Stage-2/3 extensions or unknown siblings» —
это требование не терять неизвестные поля транзитом, а не писать для них
полноценные бизнес-правила заранее. Ни одного рантайм-эффекта (запись
устройства, вычисление тепловой карты, производные HA-сущности) в дифф не
входит — это чисто валидационный код и типы, но объём (три функции, отдельные
тесты, поля в types.ts) — не «на всякий случай одна строка», а
предвосхищение ещё не начатых этапов. §10 спецификации прямо предупреждает:
«Verify the implementation/release against that boundary; do not silently
broaden it.» Нужно либо вырезать `_validate_stage2`/расширения
`_validate_settings`/связанные тесты и типы из этого PR (оставив реальный
инертный passthrough), либо получить явное подтверждение владельца, что
досрочная валидация Stage 2/3 — осознанное решение для этого раунда.
**M2 — фолбэк распознавания радара — текстовый поиск по manufacturer/model.**
`src/radar-editor.ts:107-109`:
```js
if (/\b(?:mmwave|mm-wave|radar|presence radar|fp2|fp1e|ld24(?:10|12|50))\b/.test(haystack))
return { eligible: true, profile: 'presence_v1', reason: 'radar_metadata' };
```
где `haystack` — конкатенация `device.model`/`registryDevice.model`/
`registryDevice.manufacturer`. Спецификация требует: «Match evidence is
structural adapter/registry metadata, not a friendly-name search» (§2).
Устройство, чьи `manufacturer`/`model` случайно содержат одно из этих слов,
получает полный раздел «Присутствие на плане» без проверки единой сущности —
это именно текстовый поиск по имени, просто по полю model/manufacturer, а не
по entity friendly name. Маловероятно на практике, но противоречит букве
требования и не имеет теста на ложное срабатывание.
**M3 — «Change installation» отсутствует как функция, не только как строка.**
Grep по всему диффу не находит UI-контрола `change_installation`; свежий
`installation_id`/UUID не генерируется нигде, кроме самого первого черновика
(`radarDraft`, `radar-editor.ts:177-179`, дальше переиспользует
`original.mount.installation_id` при каждом последующем сохранении). §6 прямо
требует явного действия, создающего новый installation UUID даже при
неизменных итоговых x/y («Moving a physical sensor away and back must use
Change installation even when final x/y happen to be identical») — сегодня
это в принципе недостижимо из интерфейса, а не просто не подписано. Отдельно
i18n-ключ `radar.discard_setup` («Discard the unsaved radar setup?») тоже не
существует — потому что `RadarSetupController.cancel()`/`interrupt()`
(`radar-setup.ts:121-126,329-333`) вызывают `reset()` безусловно: закрытие
«грязного» мастера после захвата контрольных точек ничего не спрашивает,
хотя §5.2 требует «Closing a dirty wizard asks to discard.»
**M4 — неоднозначный результат калибровки маскируется под «плохие
референсы».** Исключение решателя `ambiguous_sources`
(`radar-geometry.ts:88`, срабатывает, когда обе гипотезы зеркала проходят с
разницей RMS <10 см — именно случай «не выбирай молча» из AC S1-8) в
`radar-setup.ts:316` транслируется в `radar.bad_references` («Choose separated
references in different directions from the sensor»). Ключа
`radar.ambiguous_sources` нет ни в одном словаре (проверено grep). Пользователь
с корректно расставленными, но геометрически неразличимыми по зеркалу точками
получит указание переставить точки — хотя переставлять, возможно, нечего.
Это ровно тот сценарий честности калибровки, который §5.2/AC S1-8 выделяют
особо.
**M5 — «Это радар присутствия» не в конце тела диалога, как требует §2.**
Спецификация: «In the marker editor's collapsed **Additional actions** group
at the end of its body». Блок радара (включая `<details class="radar-
additional">` из `src/editors/radar-section.ts:96-101`) вставлен в
`src/houseplan-editor-runtime.ts:13116` сразу после выбора комнаты и **до**
Tap action, Controls, Value badge, Model/Link/Description/PDF и футера с
кнопками — то есть в верхней трети тела диалога, а не в его конце.
**M6 — 2 из 8 обязательных кодов ошибок никогда не возвращаются backend'ом,
и защитные гварды §5.2/AC S1-8 не покрыты тестами.**
`grep -r "unsupported_capability\|source_unavailable" custom_components/
houseplan/*.py` — ноль совпадений; все остальные 6 кодов (`invalid_radar`,
`not_ready`, `source_restricted`, `invalid_selection`, `conflict`,
`rate_limited`) реализованы и различимы. Отдельно: ни один тест (TS или
Python) не проверяет отдельно guard'ы 50 см от точки монтажа, предел RMS
≤20 см/индивидуальной ошибки ≤30 см или собственно порог неоднозначности
зеркала <10 см в `solveRadarTwoPoint`/`radar_validation.py`
(`test/radar-geometry.test.mjs` доходит только до тривиального успеха и
отклонения коллинеарных точек) — удалённый или ослабленный guard прошёл бы
все текущие тесты незамеченным, при том что AC S1-8 прямо требует «remove
each guard => invalid-fit acceptance fails» как критерий готовности.
### Low (на усмотрение автора, не блокируют)
- `docs/USER-GUIDE.md`/`USER-GUIDE.ru.md` цитируют переключатель как
«Show presence on the plan» / «Показывать присутствие на плане», а реально
выпущенный ключ — `gs.radar_show_live` = «Show live presence on the plan» /
«Показывать текущее присутствие на плане». Спецификация разрешает
переименование ключей в процессе ревью, но текст гайда тогда должен
синхронизироваться с итоговой строкой — сейчас оба языка цитируют
несуществующую в UI фразу.
- `radarHealthI18nKey`/`RADAR_HEALTH_KEYS` (`radar-model.ts:16-21`) не имеет
отображения для backend-статусов `restricted`/`incomplete` — оба
схлопываются в общий `radar.health_unknown`, маскируя «нет доступа к
источникам» под «состояние неизвестно».
- В `src/radar-editor.ts` предиктор допустимости (`recognizeRadar`) живёт не
в `radar-model.ts`, как предполагает архитектура спецификации («Proposed
`radar-model.ts` owns the pure section predicate»), а в `radar-editor.ts`.
Не бага, но отступление от предложенной, хоть и необязательной, структуры.
- Расширенная диагностика (`radar-section.ts:438-440`) не показывает
`report_age`, хотя `RadarEditorDraft.inspection.sources[].reported_at`
типизирован и получен — противоречит §2 «Advanced displays exact source
ids, values, units and report ages», но это единственное поле из
перечисленных четырёх, которого не хватает.
## Что проверено и корректно
- **S1-3** (единицы/сигналы отсутствия) — точные коэффициенты mm/cm/m/in/ft,
`unknown/unavailable/NaN/Infinity` не трактуются как ноль, x=0 и
отрицательный x легальны — подтверждено чтением и общими TS/Python
фикстурами (`test/fixtures/radar-source-boundaries.json`,
`test_radar_geometry.py`). Единственный пробел — не проверяется конфликт
задекларированной единицы с `unit_of_measurement` самой сущности (не
поднимаю до Medium: сохранение по крайней мере отклоняет неизвестную/
отсутствующую единицу, требуемого поведения по конфликту с фактическим HA
атрибутом просто нет ни в одну, ни в другую сторону).
- **S1-2/специфика LD2450** (кроме same-device, см. H3) — пара X=0,Y=0
трактуется как явное отсутствие только для `esphome_ld2450_v1`, единичный
нулевой ноль по одной оси или частичная пара — нет; `cartesian_v1` не
получает этого спецповедения. Подтверждено чтением и тестом
`test_ld2450_pair_zero_is_absent_but_single_zero_axis_is_valid`.
- **pair_quality `bounded_latest`** — окно 3с/скос 1.5с и 100мс коалесинг
реализованы точно по спецификации, слова «coherent»/атомарный фрейм нигде
не заявлены необоснованно.
- **Границы подписок/публикаций** (32 радара/пространство, ≤256 целей, 4 Гц/
радар, ≤10 inspect/мин, ≤4 subscribe/пользователь, ≤1 setup-подписка/
пользователь/маркер и ≤8 глобально) — все явно закодированы и проверяемы.
- **S1-7** (физическая точка монтажа независима от иконки) — формула проекции
в `radar-geometry.ts:21-37` совпадает с §5.1 буквально, включая знак
зеркала и масштаб `240*cell_cm`; общие фикстуры покрывают ±x, y=0, все
4 heading, зеркало, имперские единицы.
- **S1-12** (диагностический след) — `TRAIL_MS=8000`, ограничение 32 сэмпла/
слот, разрыв следа при скачке >1 м, сессионное хранение, очистка на
`_closeMarkerDialog`/скрытии страницы — всё подтверждено чтением.
- **S1-17** (ленивая загрузка) — `radar-editor.ts`/`editors/radar-section.ts`/
`radar-setup.ts` попадают только в уже существующую ленивую границу
`houseplan-editor-runtime.ts`; `radar-live.ts`/`radar-render.ts`/
`radar-model.ts` статически входят в основной View-бандл — осознанно, по
комментарию в `scripts/bundle-budget.mjs` (контроллер живого присутствия
должен быть в исходном графе View, крупный setup-UI остаётся ленивым).
Нет простаивающих таймеров/подписок при отключённом радаре — `sync()`
гейтится по capability/режиму/настройкам/наличию сконфигурированных
радаров/видимости страницы.
- **S1-15/совместимость** (`import_export.py`) — ремап/сохранение
`marker.radar.room_id`/`allowed_room_ids` при merge/duplicate/removal,
корректная зачистка блока при виртуализации маркера, «неизменённый будущий
блок переживает несвязанную запись, а изменённый — отклоняется» —
подтверждено чтением и тестом
`test_untouched_future_radar_round_trips_but_changed_one_fails`.
- **Генерация/ревизия** (§6) — `source_generation` хэширует installation_id,
mount x/y, профиль, источники, пространство; `calibration_revision` —
отдельно heading/calibration; клиентский UUID нигде не используется как
токен авторизации.
- **Трейлеры и changelog** — коммит `578df257` (User-Visible: yes) содержит
правки обоих `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` в себе же; все
последующие коммиты этого диапазона — `User-Visible: no`, что соответствует
тому, что видимое поведение не менялось после первого коммита. `Issue: #485`
проставлен во всех коммитах диапазона.
- **Golden baseline `general-color-popover-desktop-en`** — принят отдельным
коммитом `4aacf8a4` с обоснованием (рост общих настроек из-за нового
переключателя) и ссылкой `Baseline-Reviewed` на прогон CI — соответствует
процессу принятия эталонов, не самопроизвольная правка.
- **config-schema.json/config-field-registry.mjs** — новые записи
`settings.radar`/`markers[].radar` с `allowExtra`, описанием миграции и
совместимости — соответствуют требованию §6 «Extend … field registry».
## Итог
6 High и 6 Medium (все — в скоупе этой ветки). Наличие High блокирует независимо
от того, что часть архитектуры (проекция, калибровочный решатель, границы
подписок, ленивая загрузка, совместимость конфигурации) сделана добротно и
проходит собственные тесты. Возврат автору целиком одним раундом: и backend
(здоровье/teardown/same-device), и frontend (контур комнаты/зоны/потеря
конфигурации) требуют реальных исправлений кода, а не только текста;
рекомендую отдельно решить вопрос M1 с владельцем ТЗ, прежде чем чинить его
техническим вырезанием кода, поскольку это может быть намеренным дизайн-
решением, не описанным явно в комментарии реализатора.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/485-radar-presence`, коммит `f9acaa35de4f` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `e85d8ed7d8c980c191829379ead20e227dd01a37`
```
git log --all --format='%H %T' | grep e85d8ed7d8c9
```
- ТЗ `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
```