Files
houseplan-card/docs/reviews/CODE-REVIEW-218-r1.md
T
2026-08-20 11:24:46 +00:00

17 KiB
Raw Blame History

CODE-REVIEW-218-r1

  • Issue: #218 — floating-point шум одной комнаты гасит Glow всего пространства
  • ТЗ: docs/specs/218-glow-floor-geometry.md, принято на SPEC-REVIEW-218-r2 (зелёный, 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():

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