Files
houseplan-card/docs/reviews/CODE-REVIEW-485-r4.md
T
2026-09-09 01:12:38 +00:00

254 lines
23 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-r4
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485) — presence-radar
Stage 1. Этап: code (PROCESS.md §2.7). Заход r4, блокирующих циклов израсходовано
3 из 4 до этого раунда (r1 — красный, High 6/Medium 6; r2 — жёлтый, High 0/Medium 3;
r3 — жёлтый, High 0/Medium 1).
Материал: ветка `issue/485-radar-presence`, `git log --oneline origin/dev..HEAD` /
`git diff origin/dev...HEAD`, HEAD = `ca25e910876067a067b303497022d6a9a988b39f`
("test: keep radar bundle ratchet outside gzip noise") — рабочая копия уже на этом
SHA, `git fetch`/`checkout` не выполнялись.
## Почему разбор полный, а не по дельте (PROCESS §2.10)
Вердикт r3 (комментарий issue, `2026-09-09T00:32:39Z`) называет HEAD `f474793d`.
Этот SHA мёртв в текущей истории: `git cat-file -t f474793d9b5f...` →
`fatal: could not get object info`. Проверка показала тот же паттерн, что r3 уже
описала для перехода r2→r3:
- committer-время всей цепочки коммитов ветки, от `78afb689` (спецификация) до
`ca25e910` (правки этого раунда), сбито в узкий интервал `03:50:50–04:01:00 +0300`
09.09 — сквозная переигровка ветки, а не точечный `amend`;
- `git merge-base origin/dev HEAD` = `3434747d` ("build: prepare v1.73.0-beta.7
candidate") — это САМ ТЕКУЩИЙ tip `origin/dev`. Прежний merge-base на момент r3
был `687056f3`, и `687056f3` — предок `3434747d` (`git merge-base --is-ancestor`
подтверждает), то есть между r3 и этим раундом `dev` продвинулся ещё дальше:
влился #493 (`13a0bfdf`/`578cae52`/`260435e4`) и был вырезан кандидат
`v1.73.0-beta.7` (`3434747d`);
- пересечение путей `git diff --name-only 687056f3..3434747d` с путями ветки
радара (`git diff --name-only origin/dev...HEAD`) непустое и включает продуктовый
код, не только бандл/доки: `custom_components/houseplan/websocket_api.py`,
`custom_components/houseplan/validation.py`, `src/houseplan-card.ts`,
`src/houseplan-editor-runtime.ts`, `tests_backend/test_ha_websocket.py`.
Это прямо названный в процессе триггер («ребейз на ушедший вперёд dev — после
ребейза это другой код»), причём `websocket_api.py` — тот самый файл находки M-2
из r2/r3. Поэтому ниже — полный повторный разбор пересечённых файлов плюс
построчная перепроверка присутствия всех прежних находок, а не только диффа по
единственной находке r3.
## Как проверялось
Прочитан весь diff `origin/dev...HEAD` (см. состав ниже) и построчно —
пересечение путей ребейза с веткой радара, и оба новых коммита раунда:
`3f0b4aee` ("test: cover radar radial calibration guard" — закрывает M-1 из r3)
и `ca25e910` ("test: keep radar bundle ratchet outside gzip noise" — калибровка
`bundle:budget` после ребейза на #493 + пересборка бандла/отпечатка скриншотов).
**Пересечение путей ребейза с веткой радара проверено построчно, конфликтов
смысла нет:**
- `custom_components/houseplan/websocket_api.py:1459` — фикс M-2 из r2/r3
(`**({"radar_stage1_api": 1} if rt.radar_coordinator is not None else {})`)
на месте, не затронут посторонними правками #493 в этом же файле.
- `custom_components/houseplan/validation.py` — упоминания `radar` (`vol.Optional
("radar")`, строки 1818-1820, 2183) не изменились по смыслу; правки #493 в
этом файле относятся к summary-panel, не пересекаются построчно.
- `src/houseplan-editor-runtime.ts` — `radarAfterBindingChange` (фикс H6 r1) и
оба места вызова (13015, 13071 в r3; строки сместились из-за вставки
summary-panel-кода, но логика та же — `...radarAfterBindingChange(d.radar,
d.radarTouched, d.radarRemove)`) не тронуты.
- `src/houseplan-card.ts` — синхронное закрытие диалога маркера (фикс `d433b64c`
из r2, регрессия общего пути) на месте: `_closeMarkerDialog()` в
`houseplan-editor-runtime.ts:7331-7335` по-прежнему разветвляется на
`isDirty()`, синхронный путь не стал асинхронным повторно.
- `tests_backend/test_ha_websocket.py` — тест M-2
(`test_config_get_advertises_radar_only_while_coordinator_is_ready`,
строки 40-58) присутствует, не переписан слиянием с #493-тестами
(`test_summary_panel.py` — отдельный файл, не тот же диапазон строк).
**Прогнанные гейты:**
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` | зелёный |
| `node --test test/*.test.mjs` (полный фронтенд-набор) | 2378 passed, 1 skipped, 0 failed (2379 всего, `npm test`) |
| `npm run build` | собрано (rollup, 19.1s) |
| `npm run bundle:sync` | `dist` → `custom_components/houseplan/frontend` → `demo/srv/assets`; `git status` чист и до, и после — бандл воспроизводим побайтно |
| `npm run bundle:budget` | 0 — initial View 298 523 Б gzip / потолок 299 100 (±2000), запас до потолка 577 Б, до бюджета 301 066 Б — 2543 Б (предупреждение о малом запасе — унаследованный долг #367→#474, не новый) |
| `node scripts/check-docs.mjs` | «Documentation checks passed» — отпечаток скриншотов актуален после пересборки в `ca25e910` |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 60 прямых совпадений + 1 зарегистрированная связь — та же картина, что в r3 (артефакт полного диффа branch-vs-dev, не нового кода раунда: см. обоснование ниже) |
| `node demo/smoke_radar_setup.mjs` | OK, все поля отчёта `true` |
| `node demo/smoke_radar_live.mjs` | OK, все поля отчёта `true` |
| `python3 -m pytest tests_backend/test_radar_geometry.py tests_backend/test_radar_validation.py -q` (системный Python 3.12 + `pip install pytest voluptuous`) | 57 passed (14 geometry + 43 validation — на один больше r3: новый тест M-1) |
| Ручная мутация нового гварда-теста M-1 (backend): `radial_ok and rms<=20…` → `rms<=20…` в `radar_geometry.py:199` | новый тест `test_two_point_fit_rejects_hidden_radial_mismatch` красный (`DID NOT RAISE`); откат подтверждён (`git diff --stat` чист) |
| Ручная мутация того же гварда (TS): `radialOk && rmsCm<=20…` → `rmsCm<=20…` в `radar-geometry.ts:79`, пересборка `test-build` (`tsc -p tsconfig.test.json && fix-test-build.mjs`) | новый тест «rejects a reference with a hidden radial mismatch» красный (1 fail из 7); откат подтверждён |
| `git diff --name-only 687056f3..3434747d` ∩ `git diff --name-only origin/dev...HEAD` | непустое пересечение, разобрано построчно выше — не находка, конфликтов смысла нет |
| Merge-conflict маркеры (`<<<<<<<`/`=======`/`>>>>>>>`) по всему дереву | не найдены |
| Трейлеры новых коммитов раунда (`git show -s --format=full`) | `3f0b4aee`/`ca25e910` — `Issue: #485`, `User-Visible: no` на обоих (тест-фикс и внутренняя калибровка бюджета — без видимого пользователю поведения, changelog не требуется) |
| `npm run invariants` | не запускался — diff не трогает рёбра комнат/толщину/`layout`/`marker.space`/`open_spans` (`grep` по всем изменённым backend/frontend/тестовым файлам раунда и по пересечению с ребейзом — 0 совпадений) |
| CI Validate на точном SHA `ca25e910` (`34297455739`) | **«Бэкенд: pytest в Home Assistant» — completed/success, 823 passed, 2 skipped** (полный HA-харнесс, включая `test_ha_radar*.py`/`test_ha_websocket.py`); «Фронтенд: типы, юниты, мутанты, синхрон бандла» — success; HACS/hassfest/preflight — success; «Мутанты по диффу» (3 job) — **ещё in_progress** на момент публикации; смоки/perf/golden — `skipped` (heavy-гейт не по расписанию для обычного пуша, ожидаемо) |
Зелёного полного Validate на этом SHA пока нет (мутационные job не завершены) —
как и предупреждает задание, гейты выше прогнаны самостоятельно. В отличие от
r3, здесь уже есть завершённый **успешный** прогон job «Бэкенд: pytest в Home
Assistant» на точном SHA — это закрывает главный пробел r3 («Чего не
проверял»: HA-харнесс недоступен в песочнице ревью) фактическим результатом, а
не предположением автора.
## Находки
### High — нет
### Medium — нет
Единственная Medium-находка r3 (M-1: четвёртый гвард решателя `radial_ok`/
`radialOk` не покрыт тестом) закрыта предметно — см. таблицу ниже.
### Low (унаследовано, не растёт)
- Нет golden-сцены для радара (`find demo/golden -iname "*radar*"` — по-прежнему
пусто). Не эскалирую третий раунд подряд по той же логике r2/r3: защитные
утверждения (частичное здоровье, отрисовка зон, гейт допустимости калибровки,
отказ неподдерживаемых команд) уже доказаны прицельными негативными тестами.
- Планка 4 кадра/с общая на координатор, а не «на радар» буквально
(`radar.py:236-256`, поля `_pending`/`_last_publish_monotonic` — координатора).
Не новый риск, не изменился в этом раунде.
## Закрытие раунда r3
| Находка r3 | Чем закрыта | Где видно |
|---|---|---|
| M-1 — четвёртый числовой гвард решателя (`radial_ok`/`radialOk`, согласованность радиальной дистанции) не покрыт ни одним тестом ни в backend, ни в TS-двойнике | `tests_backend/test_radar_geometry.py::test_two_point_fit_rejects_hidden_radial_mismatch` (коммит `3f0b4aee`) — референс с верным направлением, но радиальной ошибкой 25 см (RMS 17.68/индив. ошибка 25,0 — оба проходят), требует `ValueError(invalid_selection)`; симметричный тест «rejects a reference with a hidden radial mismatch» в `test/radar-geometry.test.mjs` | Подтверждено ручной мутацией на обеих сторонах в этом раунде (таблица гейтов выше): снятие `radial_ok`/`radialOk` из условия красит ровно эти новые тесты и ничего больше (`DID NOT RAISE` на backend, `1 fail` из 7 на TS) |
## Унаследовано из r3 (без повторного разбора по существу — только подтверждение присутствия после ребейза)
Документ: `docs/reviews/CODE-REVIEW-485-r3.md`, SHA материала r3 `f474793d9b5f`
(мёртв после ребейза — см. «Почему разбор полный» выше; дерево `33297f22…`).
- **Все 6 High из r1** (partial-health, teardown/await race, LD2450 same-device
guard, no-contour warning vs refusal, zones render, radar binding-change
wipe) — подтверждены присутствующими построчным чтением тех же мест кода,
что называл r3 (`radar.py:552-554`, `radar.py:106-151`,
`radar_validation.py:147-180`, `radar-render.ts:18-24`,
`radar-editor.ts:65-75`/`houseplan-editor-runtime.ts` вызовы
`radarAfterBindingChange`). Ни один из этих файлов не входит в пересечение
путей ребейза этого раунда (`687056f3..3434747d`), кроме
`houseplan-editor-runtime.ts` — который перепроверен заново в этом раунде
(раздел «Как проверялось»), не унаследован вслепую.
- **M-2 (капабилити `radar_stage1_api` условна на `radar_coordinator`)** и
**M-3 (`radar.bad_references` реально достижим и промаршрутизирован
отдельно от `radar.bad_fit`)** из r2 — оба файла (`websocket_api.py`,
`radar-setup.ts`/`radar-geometry.ts`) перепроверены заново в этом раунде
(не унаследованы вслепую), потому что `websocket_api.py` входит в
пересечение путей ребейза — см. «Как проверялось».
- **i18n-структура** (`en`/`ru`/`de`/`fr`, 1363/1363 ключей без расхождений) —
принято без повторного построчного обхода: diff этого раунда не касается
`src/i18n/*.json`, а полный фронтенд-набор `npm test` (2378/2379, включает
`test/i18n.test.mjs`/`test/i18n-dead-keys.test.mjs`) прогнан заново и
зелёный.
- **Права и лимиты** координатора (ACL на подписку/инспекцию, `MAX_RADARS=32`,
лимиты подписок) — приняты без повторного чтения: `radar.py`/
`radar_websocket.py` не входят в пересечение путей ребейза этого раунда и
не менялись диапазоном коммитов `3f0b4aee`/`ca25e910`.
- **`single-source-numbers`** (радар не создаёт вторых источников чисел) —
принято по тому же аргументу: код рендера/диагностики не менялся, тест
прогнан заново в составе полного набора и зелёный (3/3 внутри 2378).
## Что проверено сверх таблиц
- **Легитимность повышения потолка `INITIAL_VIEW_GZIP_CEILING`
(299 000→299 100, коммит `ca25e910`).** Не тихое ослабление гейта:
фактическая пересборка в этой песочнице дала 298 523 Б — под новым потолком
на 577 Б, что и обосновывает комментарий коммита («восстановить >500 Б
запаса шума по обе стороны существующей 2 кБ ratchet-полосы» после того, как
ребейз на #493 сдвинул gzip-размер до 298 527 Б — почти вплотную к старому
потолку 299 000). Проверено самостоятельной пересборкой, а не принято на
слово из текста коммита.
- **Трейлеры и changelog.** `e9fb255a` («fix: close radar review gaps»,
предок обоих новых коммитов раунда, `User-Visible: yes`) правит оба
changelog в себе же — подтверждено `git show --stat`. Оба новых коммита
этого раунда (`3f0b4aee`, `ca25e910`) — `User-Visible: no`, `Issue: #485` —
корректно: ни один не меняет видимое пользователю поведение.
- **Инварианты геометрии неприменимы**: ни пересечённые с ребейзом файлы, ни
два новых коммита раунда не касаются рёбер комнат/толщины/`layout`/
`marker.space`/`open_spans`.
## Чего не проверял
- **Полный `demo/smoke_*.mjs` (234 файла)** — не гонял. `smoke-select` вернул
60 прямых совпадений, но это артефакт того же полного диффа
`origin/dev...HEAD`, что видели r2/r3 (символы `_markerDialog`/
`_editorRuntime`/`_mode`/`_cfgRev`/`cellCm` — из файлов, которые фактический
дифф этого раунда (`3f0b4aee`/`ca25e910`) не трогает вовсе: они меняют
только `test/radar-geometry.test.mjs`, `tests_backend/test_radar_geometry.py`,
бандл-артефакты, `scripts/bundle-budget.mjs` и `docs/images/screenshots.json`).
Два **прямых** радар-смока (`smoke_radar_setup.mjs`, `smoke_radar_live.mjs`)
прогнаны и зелёные; остальные 58 — код диалога/навигации, не тронутый этим
раундом и уже прогнанный прицельно в r2/r3.
- **Мутационный CI-гейт «Мутанты по диффу» (3 job)** — не завершён на момент
публикации документа (in_progress). Не жду: ручная мутация конкретно тех
строк, что добавил этот раунд (`radial_ok`/`radialOk`), выполнена лично и
задокументирована в таблице гейтов — это прямая, а не косвенная замена той
же роли для нового кода. Если по завершении job «Мутанты по диффу» на этом
SHA окажется красным на не рассмотренной мной строке — это отменяет вердикт
независимо от прочтения кода.
- **`python -m pytest tests_backend -q` (полный набор, без HA)** — не гонял
весь модуль, только `test_radar_geometry.py`+`test_radar_validation.py`
(57 тестов, прямо относящихся к дельте и к M-1). Полный backend-набор вне
HA харнесса r3 уже прогоняла целиком (451 passed) на код, который этот
раунд не меняет за пределами двух перечисленных файлов.
- **`npm run golden:verify`** — не прогонял: diff не трогает рендер; r2/r3 уже
зафиксировали отсутствие golden-сцен для радара как Low, не растущий третий
раунд подряд.
- **Perf-профили** — не запускал: diff не касается `iso-*`/`live-*`/
`render-*`/`houseplan-render-lifecycle.ts`, AC стадии не требуют
perf-профиль отдельным доказательством.
- **`node scripts/mutation-gate.mjs` локально** — не запускал; см. пункт про
CI-джобы «Мутанты по диффу» выше — сам гейт ещё выполняется в CI на этом SHA.
## Итог
0 High, 0 Medium. Единственная Medium-находка r3 (четвёртый числовой гвард
`solve_two_point`/`solveRadarTwoPoint` без теста) закрыта точечным тестом на
обеих сторонах, подтверждена ручной мутацией (гвард снят → новый тест красный
на backend и на TS). Ребейз на ушедший дальше `dev` (влился #493, вырезан
кандидат `v1.73.0-beta.7`) потребовал полного, а не дельта-разбора; пересечение
путей с продуктовым кодом (`websocket_api.py`, `houseplan-editor-runtime.ts`,
`houseplan-card.ts` и др.) проверено построчно — конфликтов смысла ребейз не
внёс, все находки r1/r2 на месте. CI Validate на точном SHA уже подтвердил
полный HA-харнес бэкенда (823 passed/2 skipped) — это закрывает главный пробел
предыдущего раунда независимым прогоном, а не заявлением автора. Вердикт —
зелёный.
<!-- material-anchors: заполняется конвейером публикации -->
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/485-radar-presence`, коммит `ca25e9108760` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `ca12394f18186c16362c75919099036c3213a5f2`
```
git log --all --format='%H %T' | grep ca12394f1818
```
- ТЗ `docs/specs/485-radar-presence-stage1.md`, блоб `322f040bfcfdba41802caad302da99188ab9d710`
```
git log --all --find-object=322f040bfcfdba41802caad302da99188ab9d710 -- 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
```
- Вердикт конвейера: `green` · High 0