Files
houseplan-card/docs/reviews/CODE-REVIEW-197-r1.md
T
claude[bot] 56f31dc199
Validate / docs (push) Failing after 38s
Validate / provenance (push) Successful in 1m11s
Validate / process-gate (push) Failing after 1m27s
Validate / changes (push) Successful in 1m20s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / backend (push) Skipped
Validate / frontend (push) Successful in 7m58s
Validate / golden (push) Failing after 9m32s
Validate / performance_smoke (push) Failing after 14m22s
Validate / smoke (push) Failing after 29m53s
docs: code review r1 for #197
Issue: #197
User-Visible: no
2026-08-19 10:12:32 +00:00

212 lines
20 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.
# Код-ревью issue #197 — цикл r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/197
- **ТЗ:** `docs/specs/197-junction-patch-fail-dark.md` (r2, зелёное
`docs/reviews/SPEC-REVIEW-197-r3.md`)
- **Диапазон:** `origin/dev...HEAD` (`db5718095624e55f6407e37f867e8e520f9c4882`,
один продуктовый коммит `fix: isolate degenerate junction patches`)
- **Ревьюер:** Claude, свежая сессия без контекста реализации
- **Вердикт:** зелёный · цикл r1/4 · High: 0 · Medium: 0
## Скоуп изменения
Единственный класс-A файл в диффе — `src/wall-thickness.ts` (+60/-6 строк).
Остальное — тесты (`test/wall-thickness.test.mjs`,
`test/fixtures/197-junction-patch.json`, `test/golden-matrix.test.mjs`),
targeted browser smoke (`demo/smoke_junction_patch_resilience.mjs`), golden
candidate-сцены (`demo/golden/matrix.mjs`, `demo/golden/harness.mjs`),
документация (`WALL-THICKNESS.md`, `ARCHITECTURE.md`, `TESTING.md`,
`USER-GUIDE.ru.md`, `STATUS.md`, оба `CHANGELOG*`) и три синхронные копии
бандла. Ни backend, ни i18n, ни манифесты не затронуты — соответствует §6 ТЗ
(«не входит в задачу»).
Продуктовое изменение в `src/wall-thickness.ts`:
1. `stableJunctionPatch(patch, coordScale)` — квантует координаты вычисляемого
junction-патча с относительным шагом `max(1, coordScale) × 10⁻¹²`,
отбраковывает нефинитные точки и патчи с площадью не больше `quantum²`.
2. `unionJunctionPatches(body, patches, coordScale, unionFn)` — транзакционно
добавляет каждый стабилизированный патч: локальный `try/catch` вокруг
одного `union`, отказ сохраняет последнее валидное `body` и не прерывает
обработку следующих патчей.
3. Цикл `for (const patch of junctions) body = body ? union(...) : ...` в
`wallBodiesGeometry()` (`src/wall-thickness.ts:1760-1761` на `dev`) заменён
вызовом `unionJunctionPatches(body, junctions, coordScale)`. Общий
структурный `try { … } catch { return null; }`, оборачивающий весь
остальной pass (exterior envelope, room rings, edge bodies, opening cuts,
extraBodies), не тронут и по-прежнему оборачивает точку вызова.
4. `virtualJunctionPatches` сделана `export` (test seam, без изменения тела) —
тот же паттерн, что и у остальных pure-geometry helpers этого модуля.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
| Unit | `npm test` | 900/900, зелёный |
| Build + сверка бандлов | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | зелёный; обе копии побайтно идентичны `dist/`, SHA-256 `b0dcd6651964c1bf9b964e750758b8dc5cad25bccf099a1b740af9b873fe68b5` — совпадает с хендофф-комментарием автора |
| Дисциплина «тест умеет падать» | временный откат `unionJunctionPatches(...)` на исходный незащищённый цикл в рабочей копии, `npm test` | ровно один новый тест (`issue #197 keeps the full masonry when one virtual-junction patch has ULP noise`) стал красным, остальные 899 остались зелёными; после возврата файла `npm test` снова 900/900. Мутация подтверждает, что регрессия реальна, а не тавтологична |
| Targeted smoke (AC7/AC8) | `node demo/smoke_junction_patch_resilience.mjs` (после свежей сборки и копирования в `demo/srv/assets`) | зелёный, все 15 проверок `true`: полный fixture (8/25/3), Plan/View/kiosk/static/hidden Iso держат один и тот же canonical `d`, paper/floor/Glow/sun barriers непустые, dark-theme toggle и HA state tick не меняют path и переиспользуют кеш (`_wallUnionCache`, fingerprint), рендер не пишет `_serverCfg` |
| Golden (доп. проверка, не входит в обязательный гейт) | `node demo/golden/run.mjs --mode=verify` (полный локальный прогон, уже был начат случайно и доведён до конца) | 0 `failed`; все существующие wall-junction сцены (`wall-junctions-plan-preview-light`, `wall-junctions-plan-t-dark`, `wall-junctions-view-dark`, `isometric-wall-junctions-dark`) — `passed`, т.е. численная стабилизация не сдвинула ни один уже принятый эталон; обе новые сцены `junction-patch-resilience-*` — ожидаемо `missing-baseline` (AC9 требует принятия только через `golden:accept -- --reviewed` на полном Linux CI артефакте, что вне этого цикла); одна несвязанная сцена `decor-color-popover-mobile-ru` — `different`, и 6 других `missing-baseline` — все они про `#180`/opening-placement, не задеты этим диффом (диффом не создаются и не меняются) |
Не прогонялось и почему:
- `npm run golden:capture`/принятие baseline — вне цикла ревью по процессу
(§8 PROCESS.md, AC9 ТЗ): baseline принимается только `golden:accept
-- --reviewed` на полном Linux CI артефакте, локальное принятие запрещено.
`golden:verify` был прогнан целиком (см. выше) и дал полезный негативный
результат — регрессий нет; этого достаточно.
- Полный набор `demo/smoke_*.mjs` (127 сценариев) — diff касается одной чистой
geometry-функции без renderer-specific веток; ограничился targeted smoke,
явно названным в AC7/АС8, плюс уже пройденным golden verify как независимым
подтверждением на уровне рендера.
- `python -m pytest tests_backend -q` — backend не тронут (0 файлов
`custom_components/**/*.py` в диффе).
- Performance-профили — ТЗ §13 явно оставляет fixture #197 вне отдельного
performance-бюджета («добавляется в performance только если измерение
покажет отдельный значимый профиль»); изменение — один дополнительный
`try/catch` и линейное квантование вершин на патч, асимптотика не меняется.
## Разбор критериев приёмки
- **AC1 (unit).** `test/wall-thickness.test.mjs:794-856` строит полный fixture
ровно по reproducer §3 ТЗ (только `rooms`/`open_spans` × `NORM_W`,
`walls[].a/b` без предварительного масштабирования), проверяет
`[8, 25, 3]`, наличие 20-см узла и вызывает production-сигнатуру
`wallBodiesGeometry(...)`. На исходном `dev` эта же комбинация даёт `null`
(см. мутационную проверку выше); после исправления —
непустые `geom`/`paperGeom` с площадями `124991.31944444453` и
`727303.8194444444`, что совпадает с числами из хендофф-комментария автора.
Доказано автотестом, тест умеет падать. **AC1 выполнен.**
- **AC2 (unit).** Строки 818-843: `virtualJunctionPatches()` даёт ровно один
патч с побайтно теми же четырьмя вершинами, что в §3 ТЗ;
`stableJunctionPatch` на исходных вершинах и на предварительно нормированных
(`/NORM_W`) даёт согласованные форкнутые координаты (`stable[1][0] ===
stable[2][0]`, `stable[2][1] === stable[3][1]`) и bounds совпадают с
исходным патчем с точностью `1e-9`. **AC2 выполнен.**
- **AC3 (unit).** Отдельный узкий тест `junction patch union isolates one
failure and continues with later patches` (строки 881-902) инжектирует
контролируемый throw через `unionFn`-параметр `unionJunctionPatches`:
первый патч падает, второй получает именно последнее валидное `body`
(`calls[1].body === 'initial-body'`), финальный результат —
`'body-with-second-patch'`; отдельно проверено, что нефинитные/нулевые по
площади патчи не доходят до boolean engine (`invalidCalls === 0`). Основной
структурный `throw` по-прежнему даёт `null` — не покрыто отдельным новым
unit-тестом, но подтверждено чтением кода: `unionJunctionPatches(...)`
вызывается внутри неизменного внешнего `try { … } catch { return null; }`
(`src/wall-thickness.ts:1771` и `:1844-1846`), который до этого изменения уже
оборачивал весь structural pass и продолжает делать это после патча;
существующие 900 тестов, включая множество null-return сценариев для
#123/#141/#150/#172, продолжают проходить без регрессии этого поведения.
Зафиксировано как «проверено чтением, не исполнением» в части общего
fail-dark; изолирующая часть доказана исполняемой мутацией. **AC3
выполнен.**
- **AC4 (unit).** Строки 806, 853-856: `before`/после сравнение
`JSON.stringify({ rooms, walls, cuts, openings, extraBodies })` до и после
вызова — побайтно равны; исходные 25 wall records не редактируются и не
сливаются перед вызовом. **AC4 выполнен.**
- **AC5 (unit).** Строки 858-878: `[...rooms].reverse()`/`[...walls].reverse()`
и вариант с разворотом каждого сегмента (`a`↔`b`) дают ту же geometry
(`geometryDifferenceArea` в обе стороны `< 1e-7`); повторный вызов с теми же
аргументами идемпотентен. **AC5 выполнен.**
- **AC6 (unit).** `npm test` зелёный на всех 900 тестах, включая существующие
матрицы corner Split (#123), junction/thickness (#141), transition (#150),
zero-divider (#172), openings, open spans, independent extras — без единой
регрессии. **AC6 выполнен.**
- **AC7/AC8 (smoke).** `node demo/smoke_junction_patch_resilience.mjs` —
зелёный, разобран выше по каждой проверке: Plan/View/kiosk/static/hidden
Iso держат идентичный masonry path; paper, clean-floor hover, Glow/sun
barriers непустые; dark-theme toggle и HA state tick не пересчитывают
topology (кеш и fingerprint переиспользуются) и не пишут config. **AC7/AC8
выполнены.**
- **AC9 (golden).** Кандидатные сцены `junction-patch-resilience-plan-dark` и
`junction-patch-resilience-view-dark` добавлены в матрицу (dark theme, Plan
и View), `test/golden-matrix.test.mjs` проверяет их состав (`[8, 25, 3]`) и
версию матрицы (`GOLDEN_MATRIX_VERSION = 30`). Baseline не принят локально
(`missing-baseline` в прогоне `golden:verify` — ожидаемо и корректно, ТЗ
прямо требует принятия только через reviewed Linux CI артефакт). **AC9
выполнен на уровне доступном код-ревью**; фактическое принятие эталона —
вне этого цикла.
- **AC10 (ревью кода).** Diff ограничен `src/wall-thickness.ts`; ни
renderer-specific обхода, ни grid-snapping физических offset-вершин
(квантование идёт по `coordScale × 10⁻¹²`, не по шагу сетки), ни
schema/backend/i18n правок, ни правок #198/#199 не найдено. Локальный
`catch` окружает исключительно один `union(current, piece)` внутри
`unionJunctionPatches`, а не остальной structural pass — основной отказ
по-прежнему долетает до внешнего catch. **AC10 выполнен.**
- **AC11 (гейты).** Таблица выше: typecheck/unit/build зелёные, три копии
бандла байтово идентичны, targeted smoke зелёный до передачи на ревью.
**AC11 выполнен.**
## Находки
Находок нет. High: 0, Medium: 0, Low: 0.
Рассмотренные и отклонённые как не-находки:
- Экспорт `stableJunctionPatch`/`unionJunctionPatches`/`virtualJunctionPatches`
как публичных функций модуля — это test seam в том же стиле, что и десятки
других уже экспортированных pure-geometry helpers этого файла (см. список
импорта в начале `test/wall-thickness.test.mjs`), а не новый runtime
diagnostic API; ТЗ §17.6 явно допускает такой путь инъекции отказа.
- Квантование применяется ко **всем** вычисляемым junction-патчам, а не
только к падающему — в спецификации (§7.1.2-4) это заложено как общее
правило, и `golden:verify` подтверждает исполнением, что это не сдвинуло ни
один уже принятый junction-эталон (`wall-junctions-*`, `isometric
-wall-junctions-dark` — все `passed`).
- Продуктовый контракт (AC10, §4/§7 ТЗ со ссылкой на `docs/specs/141-wall-
junctions.md`) соответствует итоговой реализации: изоляция ровно на
границе optional patch, а не на границе всего structural pass.
## Документация и трейлеры
Коммит `db57180` несёт `Issue: #197` и `User-Visible: yes`; changelog правки
(`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) присутствуют в том же коммите,
формулировки на двух языках согласованы и ссылаются на #197. Обновлены
`docs/WALL-THICKNESS.md` (§3, §8 — per-piece failure и numeric stabilisation),
`docs/ARCHITECTURE.md` (граница optional patch vs. structural failure),
`docs/USER-GUIDE.ru.md` (новая строка диагностики «После оптимизации исчезла
вся толстая кладка» в таблице troubleshooting — терминология согласована с
остальной таблицей раздела), `docs/TESTING.md` (новый чеклист-пункт с явной
привязкой к unit/smoke/golden) и `docs/STATUS.md` (текущий локальный цикл).
Всё — в том же коммите, что и поведение, как того требует §2.6/§11
PROCESS.md.
## Что проверено и корректно
- Численная стабилизация патча собирает ровно ту же математику, что описана
в ТЗ §7.1 и §17.1: относительный квантум, не завязанный на шаг сетки,
отбраковка нефинитных/вырожденных патчей до попытки `union`.
- Изоляция патча — честная транзакция: присваивание `current = next`
происходит только после успешного `unionFn`, при throw `current` не
меняется; проверено и unit-тестом с инжектированным отказом, и чтением.
- Общий fail-dark контракт (структурный отказ вне патчей всё ещё даёт `null`)
не нарушен — внешний `try/catch` не тронут.
- Единый canonical `wallBodiesGeometry()` остаётся источником истины для всех
перечисленных в ТЗ §7.3 потребителей — подтверждено смоуком, не только
декларацией в ТЗ.
- Persisted config не переписывается и не мутируется (immutability-проверка в
unit и `renderNeverWritesConfig` в smoke).
- Три бандл-копии синхронны, SHA совпадает с заявленным в хендоффе.
- Golden-регрессий на существующих сценах нет (полный `golden:verify`,
выполненный сверх обязательного минимума, подтвердил это исполнением).
## Чего не проверял
- Полный `demo/smoke_*.mjs` (127 сценариев за пределами
`smoke_junction_patch_resilience.mjs`) — не запускал; diff ограничен одной
чистой geometry-функцией без затронутых renderer-веток, targeted smoke плюс
выполненный (сверх обязательного) полный `golden:verify` дают
соразмерное покрытие.
- Принятие golden baseline (`golden:capture`/`golden:accept -- --reviewed`) —
вне этого цикла по процессу; baseline принимается только на полном Linux CI
артефакте.
- `python -m pytest tests_backend` — backend не тронут диффом, прогон не
требуется.
- Performance-профили (`performance_smoke`, Full Performance) — не названы в
AC, ТЗ §13 явно не требует отдельного бюджета для этого fixture; не
запускал.
- Полный `npm run inventory` (актуальные счётчики тестов для документации) —
не запускал; в этом обзоре количество тестов приведено по фактическому
выводу `npm test` (900/900), а не скопировано из чужого документа.