diff --git a/docs/reviews/CODE-REVIEW-485-r1.md b/docs/reviews/CODE-REVIEW-485-r1.md new file mode 100644 index 00000000..c4fa1ef9 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-485-r1.md @@ -0,0 +1,408 @@ +# 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». Блок радара (включая `
` из `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 с владельцем ТЗ, прежде чем чинить его +техническим вырезанием кода, поскольку это может быть намеренным дизайн- +решением, не описанным явно в комментарии реализатора. + +--- + + + +## Материал раунда + +- Ветка: `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 + ```