mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
224 lines
22 KiB
Markdown
224 lines
22 KiB
Markdown
# CODE-REVIEW-278-r1
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/278
|
||
- **Этап:** код-ревью, заход r1, блокирующих циклов израсходовано 0/4
|
||
- **Проверяемый диапазон:** `origin/dev...HEAD` (HEAD = `495043791e0fa916708f832248f13dfacf23dbdc`,
|
||
ветка `issue/278-wall-union-isolation`)
|
||
- **Спецификация:** `docs/specs/278-wall-union-isolation.md` (зелёное ревью ТЗ, r2)
|
||
- **Вердикт:** жёлтый · High: 0 · Medium: 1 (в скоупе, чинится в этой же задаче)
|
||
|
||
## Скоуп
|
||
|
||
Локальный сбой boolean-объединения одного extra-body (partition/column/draft/
|
||
exterior-shell merge) в `wallBodiesGeometry()` не должен гасить всю кладку
|
||
пространства. Диапазон реализует:
|
||
|
||
- типизированный structural result (`ok`/`degraded-extra`/`failed-core`/
|
||
`not-applicable`) с per-extra transactional изоляцией и `components[]`;
|
||
- render-safe проекцию (`wallBodiesUnionPath` → `paths[]`, отдельные SVG-path
|
||
на компонент, без объединения в один evenodd-путь);
|
||
- единый strict commit barrier (`checkSpacePhysicalGeometry` /
|
||
`_commitPhysicalGeometry`) для всех geometry writers (§7 ТЗ), с откатом,
|
||
0 WS/Undo при отказе и локализованным toast;
|
||
- parity потребителей: full Plan/View, Static, hidden Iso, Glow/light barrier
|
||
+ source guard, sun (через `roomGeom`);
|
||
- production-структурную проверку в `model-invariants.mjs` (тот же
|
||
`checkOptimizeGeometry`, что и Optimize);
|
||
- анонимизированную regression-фикстуру, unit/permutation/golden/smoke/
|
||
benchmark/mutation-покрытие, RU/EN toast и changelog/доки.
|
||
|
||
Соответствует J1/J4/J6 из `docs/SCOPE.md` — устраняет полное визуальное
|
||
исчезновение архитектуры для персоны Household members/Guests (View — «продукт»
|
||
для этих персон), не расширяя скоуп продукта.
|
||
|
||
## Как проверялось
|
||
|
||
Все дешёвые гейты (§8) прогнаны заново на этом заходе; тяжёлые — точечно, по
|
||
диффу/AC, с решением по каждой строке.
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
|
||
| Unit | `npm test` | 1204/1204 pass, 0 skip, 0 fail (см. «Расхождение с хендоффом» ниже) |
|
||
| Build + 3 копии бандла | `npm run build && npm run bundle:sync` + `cmp` `dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/` | все три побайтово идентичны |
|
||
| Docs fingerprint | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — diff трогает `src/**`, гейт обязателен |
|
||
| Model invariants (плохая фикстура) | `npm run invariants -- --config test/fixtures/278-wall-union-isolation.json` | exit 1, `wall-degraded-extra` — AC10 доказан на реальной команде, не заявлением |
|
||
| Model invariants (контрольная валидная фикстура) | `npm run invariants -- --config <276-coincident-partition config>` | exit 0, ложного срабатывания нет |
|
||
| Смок, названный в AC/диффе | `node demo/smoke_wall_union_isolation.mjs` | OK, все 11 внутренних проверок true |
|
||
| Дисциплина «тест умеет падать» | ручной мутант: `status: ... ? 'degraded-extra' : 'ok'` → `'failed-core'`, пересборка бандла, повторный прогон смока | смок падает с `TypeError: Cannot read properties of null (reading 'components')` — реальный крах, не тихий зелёный; дерево и бандлы восстановлены, `git status` чист |
|
||
| `smoke-select.mjs` (обязательная выборка) | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 34 прямых совпадения, 15 слабых; полный список см. «Смоки» ниже |
|
||
| Смоки-соседи той же геометрической подсистемы | `smoke_multiwall_junction`, `smoke_wall_key_roundtrip`, `smoke_wall_junctions`, `smoke_free_walls`, `smoke_zero_divider_taper`, `smoke_partition_openings` | все OK — регрессии в родственных фичах wall-thickness нет |
|
||
| Performance (AC13) | `node demo/benchmark_wall_union_isolation.mjs` (`test-build` пересобран `fix-test-build.mjs`) | valid overhead p95 0.099 ms (лимит 20 ms), relative p95 0.324 ms ≤ лимит 0.330 ms, degraded p95 17.6 ms (лимит 100 ms) → `pass: true` |
|
||
| Golden (визуальный результат меняется) | `npm run golden:verify` (полный прогон — флаг `--grep` не поддерживается `demo/golden/run.mjs`, ушёл в полный набор) | все просмотренные сцены `passed`, включая новые `wall-union-isolation-view-light/dark` и соседние `wall-junctions-*`, `multiwall-junction-bevel-view-dark`, `wall-key-roundtrip-view-dark`, `junction-patch-resilience-*` |
|
||
| Mutation-gate, реестр | `node scripts/mutation-gate.mjs --check` | все патчи ложатся ровно один раз, guard-файлы существуют |
|
||
| Mutation-gate, один из 5 новых id живьём | `node scripts/mutation-gate.mjs --id=wall-component-failure-kills-primary` | команда с `--id` без `=` не отфильтровала реестр и упёрлась в отсутствующий `pytest` на несвязанном backend-мутанте (окружение, не дефект #278); заменено эквивalентной ручной проверкой (см. выше «тест умеет падать» — тот же патч, тот же guard-смок, тот же результат) |
|
||
| Commit-трейлеры | `git log` по всем 8 коммитам диапазона | `Issue: #278` и `User-Visible:` на каждом; `Baseline-Reviewed` на обоих коммитах, трогающих `demo/golden/baselines/**`; `User-Visible: yes` (`8156f801`) правит `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же коммите |
|
||
|
||
### Расхождение с хендоффом (не находка, для полноты)
|
||
|
||
Хендофф автора заявляет «1204 теста: 1203 pass, 1 штатный skip». Локальный
|
||
прогон дал 1204 pass, 0 skip. Число тестов совпадает, распределение
|
||
pass/skip — нет. Похоже на окружение-зависимый skip (например, недоступный в
|
||
CI, но доступный здесь ресурс), не влияет на вердикт: интересующие #278 тесты
|
||
(`wall-thickness`, `plan-geometry-preflight`, `wall-union-isolation`) все зелёные
|
||
и не входят в подозреваемый skip.
|
||
|
||
## Находки
|
||
|
||
### Medium (в скоупе, чинится в этой же задаче)
|
||
|
||
**M1 — `docs/TESTING.md` называет несуществующие id мутантов.**
|
||
|
||
Файл: `docs/TESTING.md`, блок AC для issue #278 (после строки «Optimize и
|
||
`model-invariants` reject the same fixture…»).
|
||
|
||
Чек-лист перечисляет 5 mutation-id для этого пункта:
|
||
|
||
```
|
||
`wall-component-failure-kills-primary`,
|
||
`isolated-wall-extra-discarded`, `strict-accepts-degraded`,
|
||
`wall-thickness-writer-bypasses-barrier`,
|
||
`invariants-bypasses-production-geometry`
|
||
```
|
||
|
||
Фактический реестр `scripts/mutation-gate.mjs` (проверено `--list` и `--check`)
|
||
содержит другие имена для 4 из 5:
|
||
|
||
| В `TESTING.md` | Реально в `mutation-gate.mjs` |
|
||
|---|---|
|
||
| `wall-component-failure-kills-primary` | `wall-component-failure-kills-primary` ✓ совпадает |
|
||
| `isolated-wall-extra-discarded` | `wall-isolated-extra-discarded` |
|
||
| `strict-accepts-degraded` | `strict-wall-barrier-accepts-degraded` |
|
||
| `wall-thickness-writer-bypasses-barrier` | `wall-thickness-writer-bypasses-common-barrier` |
|
||
| `invariants-bypasses-production-geometry` | `model-invariants-bypasses-production-geometry` |
|
||
|
||
Воспроизведение:
|
||
|
||
```
|
||
$ node scripts/mutation-gate.mjs --check --id=isolated-wall-extra-discarded
|
||
мутант «isolated-wall-extra-discarded» не объявлен; --list покажет реестр
|
||
```
|
||
|
||
Тот, кто пойдёт по `docs/TESTING.md` проверять пункт #278 командой
|
||
`node scripts/mutation-gate.mjs --id=<имя из чек-листа>`, получит отказ на 4 из
|
||
5 команд. Это прямое нарушение AC15 («RU/EN changelog и user/wall/architecture/
|
||
testing/status docs актуальны») — сам код и сами мутанты корректны (реестр
|
||
проходит `--check`, а `wall-component-failure-kills-primary` независимо
|
||
воспроизведён вручную выше), дефект только в тексте `TESTING.md`. Severity —
|
||
Medium: не влияет на поведение продукта и не показывает, что сам механизм
|
||
защиты не работает, но делает канонический тест-чеклист нечитаемым машиной
|
||
и вводит в заблуждение человека. В скоупе задачи (TESTING.md — часть
|
||
release-артефактов, перечисленных в ТЗ §17), чинится правкой пяти строк в
|
||
этом же issue, без нового issue (решение владельца 2026-08-19, #202).
|
||
|
||
**Требуется:** привести 4 id в `docs/TESTING.md` к точным именам из
|
||
`scripts/mutation-gate.mjs`.
|
||
|
||
## Проверка AC1–AC15
|
||
|
||
| AC | Статус | Как проверено |
|
||
|---|---|---|
|
||
| AC1 | ✅ | `test/wall-union-isolation.test.mjs` — фикстура даёт `degraded-extra`, не global null; прогнано `npm test` |
|
||
| AC2 | ✅ | `test/wall-thickness.test.mjs` (#278 permutation test) + ручное прочтение цикла `extraBodies` в `wallBodiesGeometry()` (try/catch per-index, `isolated.push`, следующие extras продолжают обрабатываться) |
|
||
| AC3 | ✅ | код: невалидный extra (`extra.length < 3`, non-finite, `polygonArea ≤ 1e-9`) инкрементирует `degradedExtraCount` и `continue`, не бросает core; `failed-core` остаётся отдельным catch-блоком снаружи per-extra цикла — проверено чтением |
|
||
| AC4 | ✅ | `_renderWallBodies`/`space-render.ts` рисуют `united.paths`/`canonicalWallGeometry.paths` отдельными `<path>` per component (не склеены в один evenodd-path); подтверждено golden `wall-union-isolation-view-{light,dark}` (passed) и смоком |
|
||
| AC5 | ✅ | построчно проверены все 4 продакшн-потребителя: hidden Iso (`_isoSource().build()` → `united.components.flatMap`), Glow/light barrier + source-guard (`_lightBarriers` → `masonry.components` → `masonryGeometry` → `pointInOpaquePlanBody`), Static (`space-render.ts` `canonicalWallGeometry.paths`), Plan/View (`_renderWallBodies`); `roomGeom` подтверждённо не включает extras (`test/wall-thickness.test.mjs`: `roomGeom.length === 0` при двух extras) |
|
||
| AC6 | ✅ | `checkOptimizeGeometry` трактует `degraded-extra`/`failed-core` как `status:'failed'`; unit `test/wall-union-isolation.test.mjs` («Optimize и model-invariants используют один strict result») |
|
||
| AC7 | ✅ | все перечисленные в §7 ТЗ писатели физической геометрии сведены к `_commitPhysicalGeometry`/`_applyGeometryState`; инвентарный regex-тест `test/wall-union-isolation.test.mjs` («production source routes physical writers») проверяет 14 history-ключей; смок `smoke_wall_union_isolation.mjs` подтверждает rollback + toast + 0 WS/Undo вживую на бандле |
|
||
| AC8 | ✅ | decor/backdrop/marker/settings-правки (12 мест `_recordGeometry`+`_saveConfig` вне `_commitPhysicalGeometry`) прочитаны построчно — ни одно не меняет `rooms/walls/open_spans/openings/partitions/room_drafts/wall_columns`; смок: `nonGeometryEditBypassesStrictBarrier: true` |
|
||
| AC9 | ✅ | `_writeConfig()` перепроверяет `spacePhysicalGeometryFingerprint` перед фактической записью и делает 0 WS-вызовов при отказе; unit `checkSpacePhysicalGeometry`-тесты + прочтение `_pendingPhysicalWrites` lifecycle |
|
||
| AC10 | ✅ | `npm run invariants -- --config <fixture>` лично прогнан на плохой (exit 1, `wall-degraded-extra`) и на валидной контрольной (exit 0, без ложных срабатываний) фикстурах |
|
||
| AC11 | ✅ | `npm test` включает существующие #197/junction/fail-dark регрессии — все зелёные; golden `junction-patch-resilience-*`, `wall-junctions-*`, `multiwall-junction-bevel-view-dark` пройдены заново |
|
||
| AC12 | частично ✅ | реестр `--check` подтверждает целостность 5 новых id; `wall-component-failure-kills-primary` независимо воспроизведён вручную (мутант убивает `smoke_wall_union_isolation`); остальные 4 не прогнаны через `mutation-gate.mjs --id=` из-за отсутствующего `pytest` в окружении на несвязанном шаге реестра — см. «Чего не проверял» |
|
||
| AC13 | ✅ | `node demo/benchmark_wall_union_isolation.mjs` лично прогнан, все три бюджета §13 выполнены с запасом |
|
||
| AC14 | ✅ | tsc/test/build/bundle-сверка/смоки — зелёные (таблица выше) |
|
||
| AC15 | ⚠️ Medium M1 | CHANGELOG RU/EN, USER-GUIDE RU/EN, WALL-THICKNESS.md, ARCHITECTURE.md, CANVAS.md, STATUS.md — точны и соответствуют коду и терминологии `docs/USER-GUIDE.ru.md`; TESTING.md содержит 4 неверных id мутантов (M1) |
|
||
|
||
## «Одно число — один источник»
|
||
|
||
В диффе нет новой числовой величины, которую видит пользователь дважды: toast
|
||
— фиксированная строка без чисел, USER-GUIDE-правки не вводят числовых полей.
|
||
Единственные числа в диффе — внутренние performance-бюджеты (не пользовательский
|
||
UI) и хэши golden-эталонов. Правило не нарушено.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Типизированный результат `wallBodiesGeometry()`/`wallBodiesUnionPath()`
|
||
корректно заменяет старый `null`-контракт; happy-path (`status: 'ok'`)
|
||
математически не меняет прежнюю геометрию — единственный компонент
|
||
`{id:'primary', geom: primary}` даёт тот же `d`, что и раньше (подтверждено
|
||
отсутствием регрессий в golden/smoke соседних сценариев: wall-junctions,
|
||
multiwall-junction, wall-key-roundtrip, zero-divider-taper, free-walls,
|
||
partition-openings).
|
||
- Изоляция exterior-shell merge (core) отделена от изоляции extras и не
|
||
использует один и тот же `catch` — `degradedCoreCount`/`isolatedCore`
|
||
собираются отдельно, что верно отражает контракт «room masonry остаётся
|
||
fail-closed, а exterior shell может стать отдельным компонентом» (§5.4 ТЗ,
|
||
AC3).
|
||
- `#277`→`#278` порядок merge подтверждён по `git log origin/dev`: коммиты
|
||
#276/#277 уже в `dev` до ветки #278; временный adapter Resize
|
||
(`!!this._wallUnionGeometry()` / `_checkOptimizeGeometry` на весь план)
|
||
заменён на точный `_checkSpacePhysicalGeometry(candidate, preview.space)` —
|
||
риск «adapter переживает merge» (риск-таблица ТЗ №3) закрыт в этом же
|
||
коммите, не оставлен.
|
||
- `model-invariants.mjs` осознанно нарушает собственный старый принцип
|
||
«не импортирует `src/**`» — задокументировано в комментарии модуля и в
|
||
`docs/ARCHITECTURE.md`; `npm run invariants` включает шаг сборки
|
||
`test-build`, так что это не ломает автономность CLI по факту вызова.
|
||
- Фикстура `test/fixtures/278-wall-union-isolation.json` анонимна (только
|
||
синтетические id/координаты, `provenance` присутствует), не содержит
|
||
пользовательских строк.
|
||
- Мутационная дисциплина: лично собранный «зелёный, но неверный» мутант
|
||
(замена `degraded-extra`→`failed-core` в возвращаемом статусе) реально
|
||
ломает `smoke_wall_union_isolation.mjs` — findings §12 подтверждены не
|
||
только по тексту реестра.
|
||
|
||
## Чего не проверял
|
||
|
||
- 4 из 5 новых записей `mutation-gate.mjs` не прогнаны через полноценный
|
||
`--id=<mutant>` (перестройка worktree + бандла на мутанте): у самого
|
||
инструмента без флага `=` в этом окружении отсутствует `pytest`
|
||
(`/usr/bin/python3: No module named pytest`) на несвязанном backend-мутанте
|
||
раньше по реестру, и полный прогон уходит в таймаут раньше, чем добирается
|
||
до нужных пяти id. Заменено точечной ручной проверкой одного из пяти
|
||
(`wall-component-failure-kills-primary`) с тем же продакшн-смоком в роли
|
||
guard — она реально покраснела. Оставшиеся 4 (`wall-isolated-extra-discarded`,
|
||
`strict-wall-barrier-accepts-degraded`,
|
||
`wall-thickness-writer-bypasses-common-barrier`,
|
||
`model-invariants-bypasses-production-geometry`) проверены только чтением
|
||
соответствующих unit-тестов (`test/wall-thickness.test.mjs`,
|
||
`test/plan-geometry-preflight.test.mjs`, `test/wall-union-isolation.test.mjs`)
|
||
и подтверждением, что патч ложится ровно один раз (`--check`), без
|
||
фактического red/green прогона мутанта. Это предрелизный гейт (§8), не
|
||
обязательный для этого раунда, но фиксирую прямо, а не молчу.
|
||
- Не пересмотрены визуально сами PNG golden-эталонов (принято на слово
|
||
`Baseline-Reviewed`-трейлеру и тому, что `npm run golden:verify` подтвердил
|
||
соответствие текущего рендера принятым хэшам с указанными допусками).
|
||
- Не запускался `python -m pytest tests_backend` — диапазон не трогает
|
||
`custom_components/houseplan/**/*.py` (`git diff --stat` подтверждает
|
||
отсутствие таких файлов), гейт неприменим.
|
||
- Полный `demo/smoke_*.mjs`-набор (178 файлов) не прогонялся целиком —
|
||
прогнаны все «прямые совпадения», прямо относящиеся к теме (wall union/extra
|
||
bodies/geometry barrier), плюс контрольная выборка родственных
|
||
wall-thickness-смоков (6 файлов). Не прогнаны остальные ~28 «прямых»
|
||
совпадений `smoke-select`, чьи общие символы — исключительно широкие
|
||
кеш-поля (`_frame`, `_modelCache`, `_saveConfig`), не специфичные для
|
||
#278 (например `smoke_dialog_footer_width`, `smoke_pan_any_zoom`,
|
||
`smoke_canvas_frame`) — с низким риском регрессии, так как happy-path
|
||
геометрии байтово не изменился (см. «Что проверено»); 15 «слабых»
|
||
совпадений не прогонялись вовсе.
|
||
- `golden:verify` выполнен полным набором (108 сцен) случайно — флаг
|
||
`--grep` не поддерживается `demo/golden/run.mjs` (только `--scenario=`);
|
||
раз уж полный прогон уже состоялся и завершился зелёным, использую его как
|
||
дополнительное (избыточное для этого раунда) подтверждение отсутствия
|
||
визуальных регрессий, а не как обязательный для этого раунда гейт.
|
||
|
||
## Итог
|
||
|
||
Blocking (High) находок нет. Одна Medium-находка в скоупе задачи (M1,
|
||
`docs/TESTING.md`) — по правилам §2.7/#202 не создаёт отдельный issue,
|
||
возвращается автору в рамках текущего issue #278. Технический контракт,
|
||
изоляция per-extra, единый strict barrier, parity потребителей и все
|
||
проверяемые тестами AC — реализованы корректно и подтверждены не только
|
||
чтением, но и запуском гейтов и одним живым мутационным экспериментом.
|