docs: review document for #242
Validate / docs (push) Failing after 24s
Validate / reuse (push) Successful in 36s
Validate / provenance (push) Successful in 1m11s
Validate / changes (push) Successful in 1m11s
Validate / process-gate (push) Failing after 1m17s
Validate / hacs (push) Failing after 19s
Validate / hassfest (push) Failing after 20s
Validate / backend (push) Failing after 8m40s
Validate / frontend (push) Successful in 8m46s
Validate / golden (push) Failing after 15m26s
Validate / performance_smoke (push) Failing after 14m38s
Validate / smoke (push) Failing after 41m9s

Issue: #242
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-22 18:35:35 +00:00
parent 1885c779e8
commit b49983fd8d
+201
View File
@@ -0,0 +1,201 @@
# CODE-REVIEW-242-r2
- Issue: [#242](https://github.com/Matysh/houseplan-card/issues/242) — «Проём в толстой стене рисуется у произвольной грани, а не по центру толщины»
- Этап: code (PROCESS.md §2.7)
- Заход: r2 (блокирующих циклов израсходовано 1 из 4; CODE-REVIEW-242-r1 — жёлтый)
- Диапазон полного цикла: `origin/dev..HEAD` = `0033138..1885c77`
- Предыдущий раунд: CODE-REVIEW-242-r1, вердикт жёлтый, получен на SHA `5e13e34`
- Дельта этого раунда: `git diff 5e13e34..HEAD` = 2 коммита:
- `a65db39` `fix(openings): preserve gate flip turn direction` (`User-Visible: yes`) — правка H1
- `1885c77` `docs: refresh opening screenshot provenance` (`User-Visible: no`) — обновление фингерпринта скриншотов после правки, тронувшей `src/**`
- Вердикт: **зелёный**
## Скоуп
r1 закрыл жёлтым вердиктом с одной блокирующей находкой (H1, High, в скоупе) и
одной находкой в скоупе (M1, Medium): `flip_v` у ворот не менял знак 10°
поворота на общей стене двух комнат и на независимой (partition) стене —
только на однокомнатной внешней стене; и заявленный автором golden-охват
(«2 сцены») был занижен на порядок (фактически ~48). Разбор этого раунда
ограничен дельтой `5e13e34..HEAD`, потому что дельта локальна: два коммита,
оба прицельно адресуют H1 и M1, не трогают резолверы `wall-thickness.ts` /
`partition-openings.ts` (AC1/AC2/AC7) и не меняют контракт `flip_v`.
## Закрытие раунда r1
| Находка | Чем закрыта | Где это видно |
|---|---|---|
| **H1** (High) — `flip_v` не менял знак 10° поворота ворот на общей стене и на независимой стене | `a65db39` убирает вторую зеркальную инверсию `sy` для ворот в `renderOpeningVisibleGeometry` (`src/render/opening-symbol.ts`) и в `leafBasis`/`buildIsoOpeningBasis` (`src/iso-openings.ts`): `gateAngle = spec.face.side * 10 * amount` вместо `spec.face.side * sy * 10 * amount`. `face.side` уже несёт знак `flip_v` через `faceFlipV` в `houseplan-card.ts`/`space-render.ts` — второе умножение на `sy` его гасило | `src/render/opening-symbol.ts:107-117`, `src/iso-openings.ts:80-118`; подтверждено моим независимым прогоном `demo/smoke_wall_thickness.mjs` (`sharedGateFlipReversesTurn: true`, `partitionGateFlipReversesTurn: true`) и `demo/smoke_isometric_contract.mjs` (`isoGateFlipReversesTurn: true`), плюс ad hoc browser-проверкой третьего (однокомнатного внешнего) пути, который новые тесты не покрывают напрямую (см. «Как проверялось») |
| **M1** (Medium, в скоупе) — заявленный golden-охват занижен на порядок; §12.3 сцены не добавлены | `a65db39` добавляет 4 обязательные семантические сцены (`opening-symbol-room-wall-light`, `opening-symbol-diagonal-partition-dark`, `opening-symbol-flip-pairs-light`, `isometric-opening-symbol-parity-dark`) с pre-PNG semantic guard в `demo/golden/run.mjs` (`assertOpeningSymbolContract`), который до сравнения PNG проверяет центр, jamb-глубину, `op-glass`, отсутствие `scaleY`-инверсии у ворот и противоположный знак поворота в паре `flip_v`. Отдельно документирует точный список из 67 существующих golden-сцен, задетых общими фикстурами (`docs/TESTING.md`, `OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT` в `demo/golden/matrix.mjs`, застрахован unit-тестом) | `demo/golden/matrix.mjs` (`OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT`, длина 67), `demo/golden/run.mjs:486-600` (`assertOpeningSymbolContract`), `docs/TESTING.md` раздел «Opening symbol centreline (#242)», `test/golden-matrix.test.mjs` (новый тест «opening symbol goldens lock…»). Частично не сходится с фактом — см. L1 ниже |
## Унаследовано из r1
Из CODE-REVIEW-242-r1 (документ на SHA `5e13e34`) принято без повторной
проверки, так как дельта `5e13e34..HEAD` не касается этого кода:
- **AC1/AC2 для room-wall и partition путей** (центр стены, независимость от
порядка комнат/направления оси) — `src/wall-thickness.ts`,
`src/partition-openings.ts`, `src/opening-symbol-placement.ts` дельтой не
тронуты.
- **AC3** (`flip_v: true` у door/window, окно целиком едет одним `translate`)
— логика для non-gate типов не изменена (`renderSy`/`sy` для door/window
идентичны до и после `a65db39`; подтверждено тем, что затронутые тесты
`test/opening-symbol.test.mjs` для door/window не менялись в дельте, и
полный `npm test` зелёный).
- **Jambs full-depth** — `jambHalf` в `openingVisibleMetrics` дельтой не
тронут.
- **AC7 (без миграции/схемы)** — `OpeningCfg.flip_v` не тронут этой дельтой.
- **Три копии бандла и mutation-gate (кроме нового мутанта)** — методология
подтверждения не изменилась.
## Как проверялось
Прочитан `git diff 5e13e34..HEAD` построчно: `src/render/opening-symbol.ts`,
`src/iso-openings.ts` (продуктовый код), `demo/golden/harness.mjs`,
`demo/golden/matrix.mjs`, `demo/golden/run.mjs`, `demo/smoke_wall_thickness.mjs`,
`demo/smoke_isometric_contract.mjs`, `scripts/mutation-gate.mjs`,
`test/opening-symbol.test.mjs`, `test/iso-openings.test.mjs`,
`test/golden-matrix.test.mjs`, `docs/TESTING.md`, `docs/CHANGELOG*.md`,
`docs/images/screenshots.json`. Дистрибутивные копии бандла (`dist/**`,
`demo/srv/assets/**`, `custom_components/**/frontend/**`) не читались построчно
— сверены по `npm run build` (см. ниже).
Гейты, реально прогнанные в этой сессии:
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` | green |
| `npm test` | green, 1096/1096 |
| `npm run build` + сверка `dist` / `demo/srv/assets` / `custom_components/.../frontend` | green, все три sha256 = `fc19a1dce77b39b7bf67008f165980d59705b84a844f723fb84abfb185d32d16`, `git status` чист |
| `node scripts/check-docs.mjs` | green (7 файлов, 10 внешних ссылок) |
| `node scripts/mutation-gate.mjs --check` | green, включая новый мутант `opening-gate-flip-cancels-turn` |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «Зарегистрированная связь» (2): `smoke_isometric_contract.mjs`, `smoke_wall_thickness.mjs` — вывод инструмента приведён ниже |
| `node demo/smoke_wall_thickness.mjs` | green: `sharedGateFlipReversesTurn: true`, `partitionGateFlipReversesTurn: true` (и весь остальной набор), `OK` |
| `node demo/smoke_isometric_contract.mjs` | green: `isoGateFlipReversesTurn: true` (и весь остальной набор), `OK` |
| `npm run golden:verify` на HEAD (`1885c77`) | 92 сцены: 16 passed, 68 different, 8 missing-baseline; сравнено построчно с `origin/dev` |
| `npm run golden:verify` на `origin/dev` (`5bf1868`, отдельный `git worktree`) | 22 different / 66 passed / 8... фактически другой набор — использован для сравнения `actualSha256`, не для приёмки |
Вывод `smoke-select.mjs`:
```
Изменено файлов src/**: 4 · символов проекта на изменённых строках: 6
Матрица: 168 смоков · порог «широкого» символа: больше 33 смоков
Зарегистрированная связь (2):
demo/smoke_isometric_contract.mjs ← openingSymbolOffset
demo/smoke_wall_thickness.mjs ← openingSymbolOffset
```
Оба прогнаны (см. таблицу). Полная матрица (168 смоков) не прогонялась:
дельта — два файла рендера ворот плюс golden/mutation-инфраструктура, не
задевающая несвязанные подсистемы; выбор `smoke-select` уже покрывает ровно
тот геометрический путь, который правит `a65db39`.
**Дополнительная независимая проверка (не покрыта новыми тестами автора).**
Новые тесты (`test/opening-symbol.test.mjs`, `test/iso-openings.test.mjs`,
`demo/smoke_wall_thickness.mjs`, `demo/smoke_isometric_contract.mjs`,
golden-фикстура `opening-symbol-flip-pairs-light`) проверяют знак поворота
ворот только на **общей стене двух комнат** и на **независимой стене**. Ни
один из них не воспроизводит третий путь из r1 (однокомнатная **внешняя**
стена, `available[0].side` вместо константы `-1`) — единственный, который в
r1 уже работал корректно, и который эта дельта могла случайно сломать, так
как формула в обеих затронутых функциях изменена безусловно для всех ворот,
а не только для «сломанных» путей. Я independently прогнал ad hoc
browser-скрипт (тот же харнесс `demo/serve.mjs`, что и существующие
`demo/smoke_*`, не закоммичен — репозиторий трогать нельзя): толстая внешняя
стена одной комнаты (`_wallThickClick([50,250])`, cm=20), двое ворот
`flip_v: false/true`. Результат: `extGateDefaultTurn: 10`,
`extGateFlippedTurn: -10`, `extGateFlipReversesTurn: true` — путь
по-прежнему работает корректно, регрессии нет.
**Проверка «умеет ли тест падать».** Для новых unit-тестов —
`scripts/mutation-gate.mjs`'ный мутант `opening-gate-flip-cancels-turn`
восстанавливает старую формулу (`spec.face.side * sy * 10 * amount`) и гейт
подтверждает, что таргетный тест `opening-symbol.test.mjs` его ловит
(`--check` прогнан и зелёный, что означает: без патча тест проходит, с
патчем — нет; это и есть механика `mutation-gate.mjs`). Для golden semantic
guard — код `assertOpeningSymbolContract` явно бросает
`semantic golden gate flip does not reverse turn` при
`pair[0].turn + pair[1].turn) > epsilon`, что структурно не может не упасть
при регрессии знака. Для двух browser-смоков — проверено чтением: до
`a65db39` эти же строки (`sharedGateFlipReversesTurn`,
`partitionGateFlipReversesTurn`, `isoGateFlipReversesTurn`) не существовали;
их ассерты `=== -default` буквально требуют разных знаков, что при откате
патча (см. мутант выше) стало бы `false`.
## Находки
### L1 (Low) — точный список из 67 существующих golden-сцен на самом деле содержит 2 сцены, не задетые этой правкой, и не хватает 0 сцен
**Что не так.** `docs/TESTING.md` и `OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT` в
`demo/golden/matrix.mjs` заявляют «измерено сравнением `actualSha256` для
HEAD и `origin/dev`» и дают список ровно из 67 id. Я повторил это сравнение
буквально: прогнал `npm run golden:verify` на HEAD (`1885c77`) и на
`origin/dev` (`5bf1868` — это ровно `git merge-base HEAD origin/dev`, тот же
SHA, на который должен был ссылаться автор) в отдельном `git worktree`, и
сравнил `artifacts/golden/golden-report.json.results[].actualSha256`
попарно по `id`. Из 69 сцен с расхождением по 4 новым `opening-symbol-*`
исключены (их не с чем сравнивать на dev — они там не существуют); из
оставшихся 65 всё совпадает с декларированным списком, кроме двух:
`junction-patch-resilience-plan-dark` и `large-house-zoom-250-dark` заявлены
как часть списка, но их `actualSha256` **идентичен** на HEAD и на `dev`
(проверено: оба совпадающих хэша процитированы в логе) — то есть эти два
кадра не изменились из-за правки `#242`, у них обоих статус `different`
относительно baseline по совсем другой, уже существующей на `dev` причине
(та самая «unrelated pending pre-release candidates», о которой
предупреждает сам документ). Не хватает ни одной сцены — то есть под-учёта
(того, что было проблемой в M1 r1) больше нет; есть только два лишних пункта.
**Почему это Low, а не Medium.** Направление ошибки безопасное: список
переоценивает влияние (2 лишних пункта из 67), а не недооценивает его — риск
пропущенной регрессии не растёт. Расхождение не влияет ни на один AC и не
меняет видимое поведение; unit-тест (`test/golden-matrix.test.mjs`) фиксирует
именно то число и тот список, что сейчас в коде, поэтому сам гейт не
"врёт" сам себе — врёт только английская/русская проза документа и
сопроводительный комментарий о методе измерения.
**Серьёзность.** Low. Не блокирует. Можно поправить точечно (убрать два id
из `OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT` и синхронизировать
`docs/TESTING.md`) в любой последующей правке этой области; отдельный issue
не нужен ни по объёму, ни по риску.
## Что проверено и корректно
- **H1 закрыт end-to-end** на всех трёх путях host (общая стена, независимая
стена, однокомнатная внешняя стена) и в Iso — см. «Как проверялось» и
таблицу «Закрытие раунда r1». Trailers `Issue`/`User-Visible` на `a65db39`
корректны, оба changelog обновлены в этом же коммите.
- **M1 практически закрыт**: 4 обязательные по §12.3 golden-сцены добавлены с
pre-PNG semantic guard, точный (за вычетом L1) список существующих
задетых сцен задокументирован и застрахован unit-тестом, `GOLDEN_MATRIX_VERSION`
поднят до 37 и это отражено в тесте на sun-ray сценарий.
`docs/TESTING.md` явно запрещает локальный `golden:accept` для этих 71
сцены (4 новых + 67 существующих) — только через reviewed Linux-артефакт.
- **`1885c77`** — точечное обновление `docs/images/screenshots.json` и одного
PNG (`05-plan-context-tray.png`) после того, как `sourceFingerprint`
устарел из-за правки `src/**`; `check-docs.mjs` зелёный, только один
скриншот реально сменил пиксели (гейт ворот виден в тестовом фрейме), что
соответствует масштабу правки.
- **Отсутствие регрессии для door/window.** `renderSy`/`sy` в
`render/opening-symbol.ts` и `sy` в `iso-openings.ts` для типов, отличных
от `gate`, не изменились; `npm test` зелёный без единого затронутого
door/window-теста в дельте.
- **Три копии бандла и `git status`** — синхронны и дерево чисто после
build.
## Чего не проверял
- **Полный Linux golden-артефакт и его reviewed-приёмка** — вне цикла
код-ревью (§13), как и в r1. Локальный `golden:verify` обслуживает только
проверку L1/M1-охвата, не приёмку эталонов.
- **`python -m pytest tests_backend`** — не прогонял: `custom_components/**/*.py`
дельтой `5e13e34..HEAD` не тронут (только скопированный бандл
`custom_components/houseplan/frontend/houseplan-card.js`, сверенный по sha256).
- **Performance-профили** — не прогонял: дельта не меняет асимптотику,
только константу (одно сравнение типа вместо перемножения на `sy`).
- **Полная browser-smoke матрица (168 сценариев)** — не прогонял всю, кроме
двух названных `smoke-select`; см. обоснование выше. Дополнительно прогнал
вручную (не закоммичено) третий host-путь ворот, который выбор
`smoke-select` не called напрямую.
- **Унаследованные из r1 пункты** — см. раздел «Унаследовано из r1»; они не
передоказывались, так как дельта их не касается.