docs: code review r1 for #197
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

Issue: #197
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-19 10:12:32 +00:00
parent db57180956
commit 56f31dc199
+211
View File
@@ -0,0 +1,211 @@
# Код-ревью 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), а не скопировано из чужого документа.