mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Ревью #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
197 lines
17 KiB
Markdown
197 lines
17 KiB
Markdown
# 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-находок нет.
|