mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
189 lines
14 KiB
Markdown
189 lines
14 KiB
Markdown
# 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` и повторного прогона мутанта.
|