mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 06:08:59 +00:00
@@ -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». Блок радара (включая `<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
|
||||
```
|
||||
Reference in New Issue
Block a user