Files
houseplan-card/legacy/reviews/v1.66.0/CODE-REVIEW-218-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

197 lines
17 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-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-находок нет.