mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
409 lines
35 KiB
Markdown
409 lines
35 KiB
Markdown
# 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
|
||
```
|