Files
houseplan-card/docs/reviews/CODE-REVIEW-233-r1.md
T
2026-08-22 06:57:57 +00:00

197 lines
16 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 — issue #233, заход r1
- Issue: [#233](https://github.com/Matysh/houseplan-card/issues/233) — «Резайз показывает внутренние размеры, а не осевые»
- ТЗ: `docs/specs/233-resize-inner-dimensions.md`, статус **принят зелёным на r2** (`docs/reviews/SPEC-REVIEW-233-r2.md`)
- Ветка: `issue/233-resize-inner-dimensions`
- Проверяемый коммит: **`abfaae3` — feat: measure resize labels between wall faces**
(`Issue: #233`, `User-Visible: yes`)
- Заход: r1 (код), блокирующих циклов израсходовано 0 из 4
## Скоуп ревью
Полный разбор — это первый заход кода на эту задачу, дельта совпадает с
задачей целиком. Материал: `git diff origin/dev...HEAD` (16 файлов, единственный
продуктовый коммит `abfaae3` поверх принятого ТЗ), плюс переписка issue #233
(две находки спек-ревью r1 — H1 «проём сокращался бы соседями», H2 «источник
толщин назван неверно» — обе закрыты в ТЗ на r2, зелёным).
## Как проверялось
1. Прочитан принятый ТЗ (`docs/specs/233-resize-inner-dimensions.md`) целиком,
включая §6 (контракт `innerEdgeSpan`) и §7 (источник толщин `ownEdgeOffsets`
/ `roomWallProfile`).
2. Построчно сверен диф `src/wall-thickness.ts` (`innerEdgeSpan`,
`ownEdgeOffsets`) и `src/houseplan-card.ts` (`_rszEdgeLabels`,
`_rszInnerSpanCms`, `_rszScaleLabels`) с контрактом §6/§7.
3. Прогнаны гейты на дереве коммита (эта среда, в отличие от песочницы автора,
имеет Chromium — воспользовался этим и реально исполнил браузерные смоки,
а не только код-ревью):
- `npx tsc --noEmit` — чисто;
- `npm test` — **1026/1026**, зелено, включая 4 новых теста `#233`;
- `npm run build` + сверка трёх копий бандла — **md5 идентичны**
(`5ffd65073d92c4845aecb1a4145f0948` во всех трёх местах), совпадает с тем,
что заявил автор;
- `node scripts/mutation-gate.mjs --check` — **87/87 `ok`**, exit 0; все три
заявленных автором мутанта (`resize-labels-show-centreline`,
`inner-span-shortens-a-passage`, `inner-span-reads-whole-edge-thickness`)
реально красят свои тесты;
- `node demo/smoke_resize_inner_dimensions.mjs` — **OK** (реально исполнен,
не «предполагается зелёным»);
- регрессия: `node demo/smoke_draw_wall_thickness.mjs` — OK,
`node demo/smoke_wall_thickness_transition.mjs` — OK (оба смока, названные
в §12 плана автотестов как обязательная регрессия).
4. Независимая проверка мехнизма §7 (H2) вручную: собрал вне репозитория
(`/tmp/verify233b.mjs`, импорт из `test-build/wall-thickness.js`,
сгенерированного самим `npm test`, — файлов внутри репозитория не создавал)
сценарий комнаты с рёбром `(0,0)–(0.4,0)`, где `setWallThickness` записан
только на подотрезок `(0,0)–(0.2,0)` (20 см), а остаток ребра —
без записи вовсе:
- `thicknessCmAt(walls, [0,0], [0.4,0], pitch)` → **0** — подтверждает
именно тот дефект, который называла находка H2 (наивный запрос по целому
ребру ест сплит-толщину);
- `ownEdgeOffsets(...)` → `[48, 0, 0, 0]` — корректно находит толщину
20 см через атомарный профиль (48 единиц = половина от `20×24/5`),
несмотря на то, что середина ребра лежит точно на границе интервалов;
- механизм §7 работает как задумано. Это подтверждено исполнением, не
чтением.
## Находки
Обе — в скоупе задачи, обе фиксируются в этом же раунде (без High-находок это
жёлтый вердикт, отдельный issue не заводится — решение владельца 2026-08-19,
#202).
### M1 — тест AC6b не строит сценарий, который он заявляет (и который был H2)
`test/wall-thickness.test.mjs:1533-1552`, тест «`ownEdgeOffsets reads the atomic
profile, not a whole-edge lookup (#233)`»:
```js
const walls = setWallThickness([], [0, 0], [1, 0], 20, pitch);
assert.equal(thicknessCmAt(walls, [0, 0], [1, 0], pitch), 20);
```
Комментарий над тестом заявляет: «половина ребра задана толщиной, половина
нет» — но `setWallThickness` здесь вызван на **весь** осевой отрезок ребра
комнаты `(0,0)-(1,0)` (это ребро прямоугольной комнаты `[[0,0],[1,0],[1,1],
[0,1]]` целиком), а не на его часть. Поэтому `thicknessCmAt` по целому ребру
корректно возвращает 20 — это НЕ тот дефект, о котором предупреждала находка
H2 спек-ревью r1 (там `thicknessCmAt` возвращает **0** именно когда запрошено
целое ребро против **частично** заданной толщины). Наивная реализация,
читающая `thicknessCmAt` по целому ребру вместо атомарного профиля, **прошла
бы этот тест** — толщина 20 совпадает в обоих подходах, раз ребро не разрезано.
Мутант `inner-span-reads-whole-edge-thickness` (обрывает цикл `distToSeg` в
`ownEdgeOffsets` до «всегда false») ловит тест, но по другой причине —
проверяет, что путь через профиль вообще исполняется, а не что он даёт верный
ответ именно на сплит-ребре.
Воспроизведение находки (не догадка — исполнено): сценарий с настоящим
сплит-ребром (см. «Как проверялось», п.4) показывает, что `thicknessCmAt` по
целому ребру даёт **0**, а `ownEdgeOffsets` — верную половину толщины 48. Это
именно то расхождение, ради проверки которого AC6b и появился в ТЗ, но
делаемый тест его не создаёт, поэтому не может отличить исправленный код от
регресса к `thicknessCmAt`.
**Что чинить:** переписать тест, чтобы толщина была записана только на часть
ребра (например, `setWallThickness([], [0,0], [0.5,0], 20, pitch)` на ребре
длиной 1, либо аналогично численному примеру из «Как проверялось»), и
проверить, что `thicknessCmAt` по целому ребру даёт 0, а `ownEdgeOffsets` —
корректную половину толщины. Само поведение продукта уже верно (проверено
исполнением) — правка нужна только в тесте.
### M2 — не хватает мутанта из принятого §11 ТЗ
`docs/specs/233-resize-inner-dimensions.md` §11 (принят зелёным на r2)
перечисляет четыре мутационных стража:
| id | Что ломает | AC |
|---|---|---|
| `resize-labels-show-centreline` | подписи снова считают осевую длину | AC1, AC7 |
| `inner-span-ignores-neighbour-thickness` | сокращение считается только по своей толщине, соседи игнорируются | AC2 |
| `inner-span-shortens-a-passage` | правило нуля снято | AC6a |
| `inner-span-reads-whole-edge-thickness` | источник толщин подменён на `thicknessCmAt` | AC6b |
`scripts/mutation-gate.mjs` (диф `abfaae3`) добавляет только три из четырёх —
`inner-span-ignores-neighbour-thickness` отсутствует, и коммит-сообщение в
issue прямо говорит «мутанты по задаче — все **три**», тихо уронив четвёртый
пункт принятого контракта. Существующий unit-тест AC2 (`test/wall-thickness.
test.mjs:1497-1501`, `mixed = [7.5, 15, 7.5, 15]`) фактически ловит путаницу
«своя толщина вместо толщины соседа» (own=7.5 против соседей=15,15 — если бы
код использовал `own` вместо соседей, результат отличался бы, тест проверено
не проходит при такой подмене), но никакого мутационного стража, формально
доказывающего это в CI, для AC2 не существует — в отличие от остальных трёх
AC этой задачи.
**Что чинить:** добавить запись `inner-span-ignores-neighbour-thickness` в
`scripts/mutation-gate.mjs`, патчащую `innerEdgeSpan` так, чтобы сокращение
считалось по `own` вместо `offsets[edge]` в `cutAt`, и привязать её к тесту
`innerEdgeSpan measures between wall faces, not centrelines (#233)`.
## Что проверено и корректно
- **AC1–AC5, AC6** — доказаны юнит-тестами, численно сверены (285/385 см на
300×400 при стенах 15 см; диагональ не равна наивному `|b-a|-o-o`; стены
толще комнаты дают 0, не отрицательное число). Тесты реально падают на
соответствующих мутантах (проверено прогоном гейта, не по описанию).
- **AC6a (H1)** — правило нуля идёт первым в `innerEdgeSpan`
(`src/wall-thickness.ts`, `if (!(own > 0)) return centre;`), перепроверено
математически на примере из находки H1 (проём с соседями-стенами 15 см) —
сокращения нет, полная осевая длина возвращается.
- **AC6b (H2)** — сам механизм (`ownEdgeOffsets` → `roomWallProfile` →
`atomicPolyForRoom` с `wallBreaks=walls`) корректен и подтверждён
исполнением независимого сценария (см. «Как проверялось», п.4); дефект
только в тесте, который это не показывает (M1 выше).
- **AC7** — три подписи `_rszEdgeLabels` внутренние, габарит
`_rszScaleLabels` — bbox внутреннего контура. Подтверждено реальным прогоном
`demo/smoke_resize_inner_dimensions.mjs` (OK), а не только чтением кода.
- **AC8** — площадь не изменилась (тот же путь `innerContourForRoom` +
`floorMinusBodies`, диф не трогает вычисление площади ни в
`_rszEdgeLabels`, ни в `_rszScaleLabels`). Подтверждено тем же смоком
(`dragAreaStillInner`, `frameAreaAgreesWithSize`).
- **AC9** — `abfaae3` несёт `Issue: #233` и `User-Visible: yes`; оба
changelog (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) и оба
`docs/USER-GUIDE*.md` правлены в этом же коммите.
- **i18n** — диф не трогает `src/i18n/*.json`, как и заявлено в ТЗ §9 (меняется
число, не строка).
- **Golden** — не задет по построению: `_rszLive` (место, где живут эти
подписи) присваивается только на время активного жеста ресайза
(`_rszEdgeDown`/`_rszMove`/`_rszDrag`) и обнуляется на `pointerup`/отмене
(`src/houseplan-card.ts:6478,6974,8172,8224`), рендерится только внутри
`.measurelayer` (`src/houseplan-card.ts:16433-16434`) — вне активного
перетаскивания оверлея нет, статическая матрица golden его не видит.
Проверено чтением кода, не исполнением `golden:verify`.
- **Регрессия толщины стен** — `smoke_draw_wall_thickness.mjs` и
`smoke_wall_thickness_transition.mjs` зелёные на этом дереве (см. выше),
как того требует §12 плана автотестов ТЗ.
- **Touch** — задача не трогает обработчики указателя (`_rszEdgeDown`/
`_rszMove`), только текст подписи; `docs/TOUCH-SUPPORT.md` не затрагивается
по построению (проверено чтением диффа — в нём нет изменений в
pointer-путях).
## Чего не проверял и почему
- **`npm run golden:verify`** — не гонял. AC/диф не меняют видимый статический
рендер (см. выше, обоснование по коду); полный прогон — предрелизный гейт
(§8), не гейт ревью на задаче такого размера.
- **`python -m pytest tests_backend`** — не гонял, диф не трогает
`custom_components/**/*.py` (в дифф-стате таких файлов нет).
- **performance-профили** — не гонял: функция чистая, вызывается на 3 ребра за
кадр перетаскивания, не названа в AC как перфочувствительная, диф не
трогает горячие пути рендера.
- **`demo/smoke_*.mjs` вне названных в AC/§12** — не прогонял весь набор из
127 смоков: диф ограничен `wall-thickness.ts` (новые чистые функции) и двумя
методами подписей ресайза в `houseplan-card.ts`; прогнаны новый смок задачи
плюс два регрессионных, названных в самом ТЗ (§12). Более широкой
поверхности диф не касается.
## Вердикт
Механизм реализации корректен и совпадает с принятым ТЗ (проверено чтением и
независимым исполнением, включая ручную проверку самого рискового места —
сплит-толщины ребра, H2). Обе находки — про качество доказательной базы
(тест и мутационный гейт), а не про поведение продукта: ни одна не блокирует
(High: 0), обе в скоупе и чинятся правкой тестов/гейта в этом же раунде
(Medium: 2) без выхода за рамки задачи.
**Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 2 → в задаче**