Files
houseplan-card/docs/reviews/CODE-REVIEW-250-r1.md
T
2026-08-22 23:15:25 +00:00

189 lines
14 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.
# CODE-REVIEW #250 — заход r1
- Issue: [#250](https://github.com/Matysh/houseplan-card/issues/250)
- ТЗ: `docs/specs/250-opening-centerline.md`, зелёный SPEC-REVIEW r1 на `2c776b0`
(`docs/reviews/SPEC-REVIEW-250-r1.md`).
- Ветка: `issue/250-opening-centerline`
- Диапазон: `origin/dev..HEAD` = `2c776b0`, `2534916`, `0eb9ffe`, `7269550` (4 коммита)
- Продуктовый коммит: `0eb9ffe` (`Issue: #250`, `User-Visible: yes`, оба changelog).
Второй коммит `7269550` — принятый Docs screenshots артефакт (`User-Visible: no`).
## Скоуп проверки
Диапазон небольшой (production-дельта — 3 файла `src/**`, остальное тесты,
мутанты, golden-контракт, документация), поэтому разбор полный: заход первый,
предыдущего раунда нет, «Унаследовано из r<N-1>» неприменимо.
Прочитаны docs/SCOPE.md (J1/J6), тело issue #250 и все пять комментариев,
docs/USER-GUIDE.ru.md, docs/WALL-THICKNESS.md, docs/ARCHITECTURE.md,
docs/ISOMETRIC.md, ТЗ целиком.
## Как проверялось
Прочитан весь `git diff origin/dev...HEAD` построчно (29 файлов). Дополнительно
исполнено (все команды дают тот же результат, что заявлен автором):
- `npx tsc --noEmit` — green.
- `npm test` — 1114/1114 pass, 0 fail (автор сообщил 1113 pass/1 skip; локально
skip не воспроизведён — см. «Чего не проверял», не блокирует).
- `npm run build` — green; `sha256sum` трёх копий бандла совпадает друг с
другом и с хэшем `806648ae6036…`, указанным автором.
- `node scripts/check-docs.mjs` — green (7 файлов, 10 внешних ссылок).
- `node scripts/process-gate.mjs --range origin/dev..HEAD --issues` — green,
0 предупреждений.
- `node scripts/mutation-gate.mjs --id=opening-symbol-flip-restores-edge-offset`
— новый мутант пойман 1 из 1 (см. AC1 ниже).
- `node scripts/mutation-gate.mjs --id=opening-gate-flip-cancels-turn` —
**0 из 1**, см. находку CR-1.
- `node demo/smoke_wall_thickness.mjs`, `node demo/smoke_isometric_contract.mjs`,
`node demo/smoke_opening_preview.mjs` — все три green на свежепересобранном
бандле (скопирован в `custom_components/houseplan/frontend/` и
`demo/srv/assets/`).
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` —
НЕОПРЕДЕЛЁННОСТЬ (169 смоков, порог «широкого» символа >33; изменённых
символов проекта — 6: `OpeningCfg`, `OpeningFaceOffset`, `_type`, `_flipV`,
`_angle`, `_face`; ни один смок не назван инструментом напрямую). Решение
ревьюера: три выбранных автором smoke — прямое покрытие темы (wall
thickness/iso/opening preview), этого достаточно вместе с unit/golden-unit
матрицей; полная матрица 167 смоков не оправдана диффом в 6 символов на 3
файлах.
- `git diff --check origin/dev...HEAD` — чисто, whitespace ошибок нет.
- Трейлеры `0eb9ffe`/`7269550` прочитаны напрямую (`git log -1 --format=...`)
— оба корректны, `User-Visible: yes` действительно несёт оба changelog в том
же коммите.
Не прогонялось (осознанно, ниже обоснование): `npm run golden:verify` (полный),
`python -m pytest tests_backend` (backend/schema не тронуты, AC7 подтверждён
чтением — модель `flip_v` не меняется), полный `smoke_*` набор (167 смоков),
performance-профили (не названы в AC, дельта — константный helper без цикла).
## Находки
### CR-1 (Medium, в скоупе) — мутационный guard `opening-gate-flip-cancels-turn` сломан переименованием теста
`scripts/mutation-gate.mjs:1678` вызывает
`node --test --test-name-pattern="shared renderer centres defaults" test/opening-symbol.test.mjs`.
Этот тест был переименован этим же диффом
(`test/opening-symbol.test.mjs`, было `'shared renderer centres defaults and
preserves explicit door/window edge alignment'`, стало `'shared renderer
centres every flip while preserving opening direction'`) — подстрока
`"shared renderer centres defaults"` больше не встречается ни в одном имени
теста файла.
**Воспроизведение:**
```
$ node scripts/mutation-gate.mjs --id=opening-gate-flip-cancels-turn
ok чистый прогон: ... node --test --test-name-pattern="shared renderer centres defaults" test/opening-symbol.test.mjs
FAIL opening-gate-flip-cancels-turn: тест остался зелёным на сломанном коде
поймано 0 из 1
```
Причина видна напрямую: паттерн не матчит ни один подтест, `node --test`
выполняет 0 тестов и рапортует `pass 1` (файл целиком) с exit code 0 — и на
чистом дереве, и на мутированном. Guard стал тождественно зелёным независимо
от того, сломано направление ворот или нет.
Дешёвая проверка `test/mutation-gate.test.mjs` («якоря живы») этого не ловит:
она проверяет только присутствие патч-анкора в исходнике, а не то, что
`--test-name-pattern` matches. Полный `mutation-gate.mjs` (единственное место,
где это видно) — предрелизный/еженедельный гейт
(`.github/workflows/mutation-gate.yml`), не гейт этого code review; поэтому
автор, прогнавший только новый мутант по имени, не был обязан это заметить
типовым implementation-гейтом, но ТЗ §14 прямо требует: «Существующий
`opening-gate-flip-cancels-turn` сохраняется и обязан продолжать падать».
Сейчас это не так — обязательство не выполнено.
**Почему это Medium, а не High:** реальную регрессию направления поворота
ворот по-прежнему ловит обычный `npm test` (assertions теста выполняются
целиком без фильтра имени, см. `demo/smoke_isometric_contract.mjs` /
`isoGateFlipReversesTurn` — независимое дублирующее покрытие тоже green).
Пострадала только дисциплина «тест обязан уметь падать» применительно к этому
конкретному mutation-guard, не пользовательское поведение.
**Почему в скоупе, не отдельный issue:** тест переименовало само это изменение
(`test/opening-symbol.test.mjs` — файл диффа задачи), а не соседняя
подсистема; PROCESS.md/#202 требует чинить Medium-в-скоупе на месте.
**Исправление:** привести `--test-name-pattern` в
`scripts/mutation-gate.mjs:1678` в соответствие новому имени теста (например,
`"shared renderer centres every flip"`) и повторно прогнать
`node scripts/mutation-gate.mjs --id=opening-gate-flip-cancels-turn`, ожидая
`поймано 1 из 1`.
Других находок нет.
## Что проверено и корректно
- **AC1** — `openingSymbolOffset()` возвращает точный `{ox:0, oy:0}` для всех
комбинаций; сигнатура сохранена (`_type/_flipV/_angle/_face`, допущение §19.2).
`test/opening-symbol-placement.test.mjs` — единый тест-матрица: 4 типа × 2
flip × 5 углов (включая `NaN`) × 5 faces (включая `Infinity`/`NaN`/нулевую
грань) — честная замена прежних трёх сценариев, мутант ловится 1/1
(проверено исполнением, см. «Как проверялось»).
- **AC2/AC3** — Flat (`src/render/opening-symbol.ts`) и Iso
(`src/iso-openings.ts:87`, не тронут — берёт тот же offset) используют один
helper; `test/opening-symbol.test.mjs` и `test/iso-openings.test.mjs`
раздельно проверяют «один origin» и «противоположный knob направления»
(`scale(1 1)`/`scale(1 -1)`, `quarterVector[1]` знак). Прогонкой
`demo/smoke_isometric_contract.mjs` подтверждено на реальном рендере.
- **AC4** — gate: offset остаётся нулевым, знак 10° поворота меняется;
`opening-gate-flip-cancels-turn` mutant текстуально сохранён (но см. CR-1),
дублирующее покрытие (`isoGateFlipReversesTurn` smoke, полный `npm test`)
подтверждает поведение реально работает. Passage не создаёт символ — не
тронут (`type === 'passage'` ветка убрана из helper, но `renderOpeningVisibleGeometry`
для passage не рисует geometry вообще, проверено чтением).
- **AC5** — room-wall/partition parity: `demo/smoke_wall_thickness.mjs`
(`doorSavedFlipStaysCentered`) и `demo/golden/matrix.mjs` (обе grани
`positiveFace`/`negativeFace` в unit-тесте дают одинаковый ноль) — проверено
исполнением.
- **AC6** — толстая стена: `src/wall-thickness.ts` тронут только в
комментарии, `openingInnerFaceOffset` (physical half-depth/jamb) не задет;
`demo/smoke_wall_thickness.mjs` (`thickWallFullHatched`,
`thickWallStaticHatched`, `openingCutsSlab`) green — проверено исполнением.
Lock badge/hitbox/actions — не тронуты диффом (проверено чтением: ни один
файл лок-гарда/hitbox в diff не участвует).
- **AC7** — модель данных: diff не содержит правок schema/backend/migration
(`git diff --stat` подтверждает — только `src/opening-symbol-placement.ts`,
`src/render/opening-symbol.ts`, `src/wall-thickness.ts` в `src/**`); `flip_v`
остаётся тем же boolean-полем — проверено чтением.
- **AC8** — документы больше не обещают edge-align:
`WALL-THICKNESS.md`, `ARCHITECTURE.md`, `ISOMETRIC.md`, `USER-GUIDE.ru.md`,
`TESTING.md` синхронно правлены, оба changelog заполнены в том же коммите
`0eb9ffe` — прочитано построчно, `check-docs` green.
- **AC9** — typecheck/test/build green, три бандла побайтово идентичны —
подтверждено исполнением (SHA `806648ae6036…` совпал).
- **Golden-контракт**: `demo/golden/matrix.mjs`/`harness.mjs`/`run.mjs`
заменяют `offset: 'edge'` на `'center'` и убирают саму опцию `'edge'` из
допустимых значений; `test/golden-matrix.test.mjs` проверяет обновлённый
контракт четырёх сцен и счётчик impact-сет = 67 (совпадает с числом,
заявленным автором) — прогнано вместе с `npm test`, green.
- **Мутанты (реестр)**: три устаревших якоря (`opening-symbol-default-uses-room-face`,
`opening-symbol-partition-follows-endpoints`, `opening-gate-flip-translates-leaves`)
корректно удалены из `scripts/mutation-gate.mjs` и нигде больше не
упоминаются (`grep` по всему дереву) — контракт §14 ТЗ выполнен, кроме CR-1.
- Трейлеры и changelog-требование `AGENTS.md`/PROCESS.md — оба коммита
корректны.
## Чего не проверял
- Полный `npm run golden:verify` (77 different, 2 error, 3 missing — цифры
автора не перепроверены построчно): предрелизный гейт по ТЗ §13.3, локальный
accept всё равно запрещён; не влияет на вердикт code review.
- Полный `smoke_*` набор (167 смоков) — только три выбранных вручную плюс
вывод `smoke-select` (НЕОПРЕДЕЛЁННОСТЬ) учтены; более широкий прогон не
оправдан 6-символьной дельтой в 3 файлах.
- `python -m pytest tests_backend` — backend/schema не в диффе (AC7 разобран
чтением).
- Расхождение «1113/1 skip» (автор) vs «1114/0 skip» (у меня) в `npm test` —
вероятно, разница окружения (Chromium/иное), не влияет на код; не
расследовалось глубже, так как оба результата green по существу задачи.
## Вердикт
Единственная находка — CR-1, Medium, в скоупе задачи. High нет. Продуктовое
поведение (offset, направление, jamb, parity, документация, changelog)
подтверждено и исполнением, и чтением, полностью соответствует ТЗ и решению
владельца. Возврат автору для точечного исправления `--test-name-pattern` в
`scripts/mutation-gate.mjs` и повторного прогона мутанта.