mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,196 @@
|
||||
# CODE-REVIEW-218-r1
|
||||
|
||||
- **Issue:** [#218](https://github.com/Matysh/houseplan-card/issues/218) — floating-point шум одной комнаты гасит Glow всего пространства
|
||||
- **ТЗ:** [docs/specs/218-glow-floor-geometry.md](../specs/218-glow-floor-geometry.md), принято на [SPEC-REVIEW-218-r2](SPEC-REVIEW-218-r2.md) (зелёный, r2/2)
|
||||
- **Диапазон:** `origin/dev...HEAD`, продуктовый коммит `3d11758` (единственный коммит с изменением кода; `c6ff34c`…`4c512fa` — ТЗ и его ревью)
|
||||
- **Ветка:** `issue/218-glow-floor-geometry`
|
||||
- **Цикл:** r1/4
|
||||
|
||||
## Скоуп
|
||||
|
||||
Единственный продуктовый коммит `3d11758` (`Issue: #218 · User-Visible: yes`):
|
||||
|
||||
- `src/physical-geometry.ts` — `normalizeBooleanBody()`, квантование входа `unionBodies()`,
|
||||
покомнатный fallback `intersectionPathsByBound()` внутри `intersectionPaths()`;
|
||||
- `src/houseplan-card.ts` — `_warnGlowGeometryFallback()` (ограниченный dedupe-`Set`),
|
||||
проброс `onBoundsFailure` из `_renderGlowLayer()`;
|
||||
- `test/physical-geometry.test.mjs` — ULP/malformed-room/overlap/permutation unit;
|
||||
- `demo/smoke_glow_geometry_resilience.mjs` — новый таргетированный browser smoke;
|
||||
- `scripts/mutation-gate.mjs` — 4 новых мутанта под #218;
|
||||
- `docs/LIGHT.md`, `docs/TESTING.md`, оба `CHANGELOG*`, `docs/images/screenshots.json` (fingerprint);
|
||||
- три сгенерированных копии бандла.
|
||||
|
||||
Диагноз причины в ТЗ (§3) уже был построчно проверен на этапе ревью ТЗ по
|
||||
`physical-geometry.ts:224-275` / `houseplan-card.ts:14880-15117` — здесь не
|
||||
переповторяю, а проверяю, что реализация действительно закрывает принятый
|
||||
контракт.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Дерево чистое на момент ревью (`git status` пуст и после прогонов).
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Дешёвые (всегда) | `npx tsc --noEmit` | зелёный |
|
||||
| | `npm test` (`npm run inventory`: Node unit 961) | 961/961, 0 fail |
|
||||
| | `npm run build` + `cmp` трёх копий бандла | идентичны (sha256 `569b5a6…`) |
|
||||
| По необходимости | `npm run golden:verify` | все geometry/wall/opening/isometric/lighting/glow-сценарии зелёные (см. ниже) |
|
||||
| | `node demo/smoke_glow_geometry_resilience.mjs` | `OK`, все 7 полей `true` |
|
||||
| | `node demo/smoke_glow_fail_dark.mjs` | `OK`, все 4 поля `true` |
|
||||
| | `node demo/smoke_glow.mjs`, `smoke_glow_blending.mjs` | `OK` (из плана §17.2) |
|
||||
| | `node scripts/mutation-gate.mjs --id=<4 id #218>` | все 4 мутанта: «покраснел, как обязан» |
|
||||
| | `node scripts/process-gate.mjs` | пройден, 0 предупреждений |
|
||||
| | `node scripts/check-docs.mjs --external` | пройден |
|
||||
| Не гонялся | `python -m pytest tests_backend -q` | диапазон не задевает `custom_components/**/*.py` |
|
||||
| Не гонялся (см. «Чего не проверял») | performance-профиль `large-house-glow-overlay-v1` | требует CI/exact-SHA инфраструктуры, локальный прогон по документации проекта не является гейт-доказательством |
|
||||
|
||||
`golden:verify` запущен, потому что диффа затрагивает общую `unionBodies()`,
|
||||
используемую не только Glow, но и `physicalBodySet`/`floorMinusBodies` (стены,
|
||||
колонны, clean floor) — риск из §15 ТЗ явно требовал не ослабить их. Полный
|
||||
браузерный набор (127 смоков) не гонялся: диагноз локализован в геометрии
|
||||
пола/Glow, а не во всём поверхностном наборе карточки.
|
||||
|
||||
## Находки
|
||||
|
||||
### Low — двойной проход нормализации на здоровом пути (не блокирует, снято с записью)
|
||||
|
||||
`src/physical-geometry.ts:344-361`. В `intersectionPaths()`:
|
||||
|
||||
```ts
|
||||
const hasRejectedBound = bounds.some((bound) => !normalizeBooleanBody(bound));
|
||||
const limit = hasRejectedBound ? null : unionBodies(bounds);
|
||||
```
|
||||
|
||||
`hasRejectedBound` прогоняет `normalizeBooleanBody()` по каждому `bound` (без
|
||||
короткого замыкания, пока не найдётся брак — в здоровом случае оно не
|
||||
найдётся никогда), а затем `unionBodies(bounds)` **повторно** нормализует те
|
||||
же тела внутри своего `.map()`. Итог — не «не больше одного дополнительного
|
||||
линейного прохода по вершинам», как буквально обещано в §14 ТЗ, а два прохода
|
||||
на обычном (не fallback) пути.
|
||||
|
||||
**Почему не блокирует:** класс сложности не меняется (по-прежнему O(vertices)
|
||||
перед уже существующей булевой операцией — сам union/intersection на
|
||||
порядок дороже одного линейного прохода квантования), путь дополнительно
|
||||
защищён `_glowClipCache` (пересчитывается только на промах кеша — смена
|
||||
геометрии/позиции/радиуса, не на каждый кадр анимации непрозрачности/цвета).
|
||||
Реального нарушения бюджетов `budgets-glow-smoke.json` это правдоподобно не
|
||||
даёт. Формальное обещание «не больше одного прохода» нарушено буквой, а не
|
||||
следствием — снимаю как Low. Тривиальное исправление на будущее: вычислить
|
||||
нормализованные `bounds` один раз и передать их в `unionBodies`, либо завести
|
||||
`unionNormalizedBodies(bodies: (number[][] | null)[])`.
|
||||
|
||||
Других находок нет.
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- **AC1 (ULP-нормализация).** `normalizeBooleanBody()` — квантование `1e-6`,
|
||||
канонизация `-0`, снятие соседних дублей, отбраковка вырожденного кольца
|
||||
(< 3 разных точек или площадь ≤ `step²`), **вход не мутируется** (проверено
|
||||
чтением + unit `structuredClone`/`deepEqual` против оригинала). Мутант
|
||||
`union-quantization-removed` (убирает `Math.round`) красит именно тесты
|
||||
`boolean input normalization` и `six-room ULP` — доказательство того, что
|
||||
тест умеет падать, получено запуском, а не на слово.
|
||||
- **AC2 (реальная regression-фикстура).** `noisySixRoomFloor` в
|
||||
`test/physical-geometry.test.mjs` и в `demo/smoke_glow_geometry_resilience.mjs`
|
||||
— те же две проблемные пары координат (`0.46666666666666673` /
|
||||
`0.4666666666666667`, `0.7083333333333335` / `0.7083333333333334`) из
|
||||
реального экспорта, без имён/entity/конфига. Юнит: `paths.length > 0`.
|
||||
Смок: `ulpFloorKeepsGlow: true`, `ulpNoiseNeedsNoFallback: true` (не просто
|
||||
«непусто», а именно «без захода в fallback» — нормализация чинит проблему
|
||||
на обычном пути, как и требует §8.2 п.1). Красный-до-фикса для этого пути
|
||||
подтверждён тем же мутантом `union-quantization-removed`.
|
||||
- **AC3 (локальная деградация).** Юнит `one malformed room is diagnosed and
|
||||
cannot erase healthy lit floor`: намеренно самопересекающийся полигон не
|
||||
проходит `union`, здоровая комната остаётся освещена
|
||||
(`paths === ['M 3 0 L 5 0 L 5 2 L 3 2 Z']`), `onBoundsFailure` вызван ровно
|
||||
для брака (`{ boundIndex: 1, phase: 'bound-union' }`). Отдельно проверено:
|
||||
«все комнаты биты» → `[]`, без утечки сырого fan. Мутант
|
||||
`union-failure-kills-space` (заменяет покомнатный fallback на `return []`)
|
||||
красит `malformed room`/`fallback unions overlapping` — подтверждено
|
||||
запуском.
|
||||
- **AC4 (диагностический след).** `_warnGlowGeometryFallback()` — ключ
|
||||
`space|fingerprint|room`, `Set` с бордюром 128 и вытеснением самого старого.
|
||||
Смок явно очищает `_glowClipCache` и повторно рендерит с тем же
|
||||
fingerprint: `fallbackWarningDeduplicated: true` (ровно одна запись,
|
||||
`_glowGeometryWarnings.size === 1`), `fallbackWarningIsRedacted: true` (нет
|
||||
имени комнаты, entity id, текста `Unable to complete…`, координатных
|
||||
паттернов). Мутант `union-failure-silent` (гасит `console.warn`) ловится
|
||||
тем же smoke — подтверждено запуском.
|
||||
- **AC5 (fail-dark).** Пре-существующий unit `intersection failure is
|
||||
fail-dark and never returns the unclipped fan` не изменён и зелёный.
|
||||
`smoke_glow_fail_dark.mjs` зелёный без правок (источник в кладке, экран без
|
||||
пола, полный отказ — везде `false`/пусто). Мутант `glow-fail-dark-weakened`
|
||||
(отключает `pointInOpaquePlanBody` guard) красит именно этот smoke —
|
||||
подтверждено запуском, граница безопасности не ослаблена.
|
||||
- **AC6 (неизменность валидных планов).** `golden:verify` — все
|
||||
`lighting-*`, `geometry-*`, `opening-*`, `isometric-*`, `junction-*`,
|
||||
`split-*` сценарии зелёные (полный список см. таблицу гейтов). Две
|
||||
«different»/«missing-baseline» находки (`device-icon-state-table-*`,
|
||||
`device-text-shell-long-*`) не относятся к #218: они существуют уже на
|
||||
`origin/dev` (tip `9e8393f` = коммит ревью #217, `merge-base` с этой веткой
|
||||
совпадает 1:1 с `origin/dev`), это необновлённый baseline после #217,
|
||||
ожидающий `golden:accept --reviewed` перед бетой — вне скоупа этой задачи.
|
||||
Input-immutability подтверждена unit-тестом и source review (`unionBodies`/
|
||||
`normalizeBooleanBody` работают на копии). Config/schema не менялись
|
||||
(source review диффа: `custom_components/**` не тронут).
|
||||
- **AC7 (Glow-base).** Смок: `glowBaseKeepsAllRooms: true` — все 6
|
||||
`.glow-base` присутствуют на «шумной» fixture. Прочитал код: `_cleanFloor()`
|
||||
→ `floorMinusBodies()` → `unionBodies()` — тот же общий boolean boundary,
|
||||
который правит #218, поэтому наблюдение «0 из 6» закрывается общей
|
||||
стабилизацией без отдельного renderer, как и разрешает §10 ТЗ (ветка
|
||||
«тест красный из-за того же raw-coordinate boolean path»). Implementation
|
||||
evidence в issue явно эту причинно-следственную связь не проговаривает
|
||||
(перечисляет смок, но не называет механизм) — не считаю это отдельным
|
||||
Low: причина проверена мной чтением кода, а не заявлением автора, что и
|
||||
требуется от код-ревью.
|
||||
- **AC8 (детерминизм).** Юнит `six-room ULP topology...permutation-stable`:
|
||||
прямой и развёрнутый порядок тел дают площадь с разницей ≤ `BOOLEAN_COORD_QUANTUM²`;
|
||||
фиксированное значение площади (`0.31835083680549986`) закрывает регресс
|
||||
самого квантования.
|
||||
- **AC9 (perf/privacy) — частично, см. находку выше.** Fast path не
|
||||
превышает O(vertices + существующие boolean-операции) — класс сложности не
|
||||
меняется (Low-находка про двойной проход это не Big-O регрессия). Redacted/
|
||||
dedup warning проверены в AC4. Целевой performance smoke не запускался —
|
||||
см. «Чего не проверял».
|
||||
- **AC10 (release-артефакты).** Оба `CHANGELOG*` в том же коммите (трейлер
|
||||
`User-Visible: yes` соблюдён), `docs/TESTING.md` — новый пункт чек-листа с
|
||||
точными командами и mutation id, `docs/LIGHT.md` — абзац про `1e-6`-сетку и
|
||||
покомнатный fallback, `docs/specs/README.md` — таблица issue↔ТЗ. Три копии
|
||||
бандла байт-в-байт идентичны (`cmp`, sha256 сверены лично). `screenshots.json`
|
||||
fingerprint обновлён консистентно (все сценарии — новый `sourceFingerprint`);
|
||||
`check-docs.mjs --external` зелёный.
|
||||
- **Процесс.** `process-gate.mjs` (офлайн) — 0 предупреждений: трейлеры,
|
||||
имя ветки, наличие `docs/specs/218-*.md`, единственный `Release:`-независимый
|
||||
D-коммит отсутствует (тут его и не требуется).
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- **`python -m pytest tests_backend`** — диапазон не содержит ни одного файла
|
||||
`custom_components/houseplan/**/*.py`; неприменимо.
|
||||
- **Полный performance-профиль `large-house-glow-overlay-v1` /
|
||||
`budgets-glow-smoke.json`.** AC9 называет «targeted performance smoke»
|
||||
доказательством, и диффа действительно трогает путь, который эти бюджеты
|
||||
меряют. Не прогонял: `demo/performance/README.md` сам называет только
|
||||
exact-SHA Linux-прогон `performance_smoke`/`performance.yml` гейт-доказательством,
|
||||
а локальный отчёт — диагностикой; полный набор — предрелизный гейт по
|
||||
PROCESS.md §8/§11.4, а не гейт код-ревью. Компенсация: source review
|
||||
показал, что класс сложности пути не меняется (см. Low-находку), а
|
||||
`_glowClipCache`/`_glowGeometryWarnings` остаются ограниченными структурами
|
||||
— это качественная, не измеренная, гарантия.
|
||||
- **Полный набор из 127 браузерных смоков и полный HA-harness** — диагноз и
|
||||
правка локализованы в булевой геометрии пола/Glow; прогнаны все смоки,
|
||||
прямо относящиеся к затронутой поверхности (`smoke_glow*`,
|
||||
`smoke_glow_fail_dark`, новый таргетированный), плюс `golden:verify` целиком
|
||||
из-за общего `unionBodies()`. Остальные 120+ смоков (устройства, тач,
|
||||
диалоги и т.д.) поверхности не касаются.
|
||||
- **Ручной запуск карточки в браузере вне demo-harness** — не входит в этот
|
||||
цикл; browser smoke (`demo/smoke_glow_geometry_resilience.mjs`) — canonical
|
||||
замена ручного прогона по PROCESS.md.
|
||||
|
||||
## Вывод
|
||||
|
||||
Все 10 AC либо доказаны автотестом с подтверждённой способностью падать
|
||||
(мутационный гейт запущен лично для всех 4 заявленных мутантов и все пойманы),
|
||||
либо разобраны чтением кода там, где автотеста не требуется (AC7 — причинная
|
||||
связь через `floorMinusBodies`). Единственная находка — Low, снимаю с записью,
|
||||
без правки. High-находок нет, Medium-находок нет.
|
||||
Reference in New Issue
Block a user