Files
houseplan-card/docs/reviews/CODE-REVIEW-303-r1.md
T
2026-08-25 10:05:08 +00:00

231 lines
20 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-303-r1 — #303: подсветка «Толщины стены» по факту кладки
- Issue: [#303](https://github.com/Matysh/houseplan-card/issues/303)
- Этап: `code` (PROCESS.md §2.7)
- Трек: `small` — ТЗ живёт в теле issue (комментарий Matysh, 2026-08-25T07:48:43Z),
ревью ТЗ — комментарий-вердикт зелёный, документ `docs/reviews/SPEC-REVIEW-303-r1.md`
- Диапазон: `origin/dev...HEAD` — `origin/dev` = `0f71d86`, `HEAD` = `93cd455`
(ветка `issue/303-wallthick-hover-width`)
- Коммиты: `36992d68` (`fix: match wall thickness hover to masonry`, `Issue: #303`,
`User-Visible: yes`), `93cd4556` (`docs: refresh reviewed screenshots`,
`Issue: #303`, `User-Visible: no`)
- Заход: **r1** · блокирующих циклов ревью 0/2 (лёгкий трек — лимит 2, PROCESS.md §4)
- Вердикт: **зелёный**
## Скоуп ревью
12 файлов, +217/−24.
Продуктовый код (класс A): `src/grid-scale.ts` (+16 — новая чистая функция
`wallThickHoverHalfUnits`), `src/houseplan-card.ts` (+8/−6 — `_wallThickHover`
переведён на неё, пол `gridPitch*1.25` удалён; `_wallThickHit` не тронут).
Гейты/тесты (класс B): `test/grid-scale.test.mjs` (+29), новый браузерный смок
`demo/smoke_wallthick_hover_width.mjs` (+87), три новых мутанта в
`scripts/mutation-gate.mjs` (+46).
Документация (класс C): `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` (+5/+5,
`User-Visible: yes` в том же коммите — верно), `docs/TESTING.md` (+7 — новая
строка чек-листа со ссылками на unit/smoke/мутанты).
Генерируемое (класс D): `dist/houseplan-card.js`,
`custom_components/houseplan/frontend/houseplan-card.js` — сверено байт-в-байт
пересборкой (см. таблицу гейтов); `docs/images/09-device-info.png` и
`docs/images/screenshots.json` — обновлённый `sourceFingerprint` после смены
`src/**`, второй коммит корректно помечен `User-Visible: no`.
Backend (`custom_components/houseplan/**/*.py`), манифесты, i18n, README,
`docs/USER-GUIDE.ru.md` — не тронуты (`git diff --stat origin/dev...HEAD`
подтверждает; § «Что не входит» ТЗ явно исключает превью рисования стены и
другие инструменты).
Прочитано до вердикта: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.7/§2.10/§4,
тело issue #303 и все 4 комментария (ТЗ, зелёное ревью ТЗ, «взял в разработку»,
отчёт о готовности), `docs/reviews/SPEC-REVIEW-303-r1.md`, `docs/WALL-THICKNESS.md`
(упоминает инструмент «Толщина» и hover, не фиксирует контракт ширины полосы —
без противоречий), `docs/USER-GUIDE.ru.md` (терминология «Толщина стены»
совпадает с формулировкой changelog), весь изменённый продуктовый код и тесты
целиком.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | green (без вывода) |
| Сборка | `npm run build` | green, `dist/houseplan-card.js` пересобран |
| Синхронность бандлов | `cp dist/… custom_components/…/houseplan-card.js && npm run bundle:sync`, затем `git status --porcelain` | пусто — пересборка из чистого чекаута дала байт-в-байт то же содержимое, что закоммитил автор; все три копии синхронны |
| Unit | `npm test` | **1294 теста: 1293 pass, 1 skip, 0 fail** (см. примечание ниже) |
| Docs-гейт | `node scripts/check-docs.mjs` | green — «Documentation checks passed (7 files, 10 external links)» |
| Мутации, точечно (реестр §10.2, не полный прогон — по дельте) | `node scripts/mutation-gate.mjs --id=wallthick-hover-floor-back`, `--id=wallthick-zero-strip-not-visual`, `--id=wallthick-hit-narrowed` | **3/3 «тест покраснел, как обязан»** — под каждой мутацией именно новый unit/smoke гаснет |
| Применимость патчей всех мутантов | `node scripts/mutation-gate.mjs --check` | green (не дал закончиться полному прогону, см. ниже — досрочно остановлен) |
| Целевой смок AC | `node demo/smoke_wallthick_hover_width.mjs` | green — все 5 проверок `true` |
| Смоки по прямому совпадению символа (`node scripts/smoke-select.mjs --base origin/dev --head HEAD`) | `smoke_decor.mjs`, `smoke_grid_scale_invariance.mjs`, `smoke_space_scale_defaults.mjs` (все три — совпадение по `cellCm`) | все **green** |
**Примечание по unit-тестам.** Автор отчитался «1292 pass, 2 skip»; в моём
прогоне — 1293 pass, 1 skip. Разница на один тест — единственный skip в моём
прогоне (`#904 issue 281 private exact fixture is not present`) отмечен как
условно пропускаемый при отсутствии приватной фикстуры; он не относится к
диапазону #303 и не влияет на результат (0 fail в обоих случаях). Расхождение
в счёте — особенность окружения, не находка по этому диапазону; фиксирую как
пример того, что «verified» без точной команды/среды может на единицу
расходиться, но здесь это не заслоняет главный факт — красных тестов нет.
`node scripts/mutation-gate.mjs --check` без ограничения по `--id` прогоняет
«чистый прогон» guard-команды **для каждого из ~40 существующих мутантов**
реестра (пересборка/тесты на каждый) — это дублирует ежедневный `npm test` и
принадлежит пред-релизному гейту (комментарий в самом скрипте: «его место —
перед стабильным релизом, не на каждой бете»). Остановил его после того, как
подтвердил, что новые три мутанта в списке проверок присутствуют и «чистый»
прогон их guard проходит; предметная проверка «умеет падать» сделана точечными
`--id=` прогонами выше — это и есть проверка по дельте, а не по всему реестру.
### Диапазон трогает геометрию?
Нет. Диапазон читает существующее поле `hit.cm` (толщина стены, уже посчитанная
`wallIntervals`) только для отрисовки; не меняет структуры `walls`, `layout`,
`marker.space`, `open_spans` и не пишет новых геометрических данных. `npm run
invariants` не запускал — по построению диапазона гейт неприменим (нет записи/
чтения ссылок на геометрию, только визуальный проход по уже готовому значению).
## Мутанты
- `wallthick-hover-floor-back` — возвращает старый пол `max(depth/2,
gridPitch*1.25)`; гвард — точечный unit `«uses the exact physical wall
width»` в `test/grid-scale.test.mjs`. Прогнан: **покраснел**.
- `wallthick-zero-strip-not-visual` — убирает `gridVisualUnits` из ветки
нулевой толщины (заменяет её на голый `gridPitch*1.5`, ломая инвариантность
к масштабу); гвард — unit `«zero-thickness hover»`. Прогнан: **покраснел**.
- `wallthick-hit-narrowed` — сужает `pull` в `_wallThickHit` с `gridPitch*6`
до `gridPitch*2`; гвард — `demo/smoke_wallthick_hover_width.mjs`. Прогнан:
**покраснел**.
Все три мутанта корректно нацелены (регрессия из issue, регрессия того же
класса, риск смежного контракта — сужение зоны попадания) и реально ловятся
названными тестами, а не проходят «случайно».
## Находки
Нет находок High или Medium. Низкая — одна, ниже.
### L1 — счёт unit-тестов в отчёте разработчика на единицу отличается от факта
**Файл:** отчёт в комментарии issue (не код).
**Серьёзность:** Low.
Разработчик указал «1294 unit-теста (1292 pass, 2 skip)»; фактический прогон в
среде ревью — 1293 pass, 1 skip, 0 fail. Причина, скорее всего, — среда:
условный skip `#904` (приватная фикстура #281) присутствует не всегда. Это не
влияет на диапазон #303 (тест не относится к нему, fail нигде нет) и не меняет
вердикт.
**Диспозиция:** снимается без правки — расхождение в подсчёте пропусков
природы окружения, не дефект кода; ноль red в обоих случаях.
## Что проверено и корректно, по AC ТЗ
- **AC1** (`cell_cm:30`, стена 50 см — полоса совпадает с телом, допуск 2%).
Доказано исполнением: `smoke_wallthick_hover_width.mjs` строит план,
выставляет `cell_cm:30`, ставит стену 50 см, затем поперечным сканом
`isPointInFill` меряет ширину `.wallbody` и `.wallthick-hover` в одной и той
же точке оси — `bodyIsPhysicalWidth` и `hoverMatchesWallWithinTwoPercent`
зелёные. Дополнено чтением: `wallThickHoverHalfUnits` для `cm>0` вызывает
тот же `wallCmToUnits`, что и тело стены (`_wallUnionGeometry` /
`drawWallPreviewD`) — совпадение не случайное, оба потребителя читают одну
формулу, один источник числа.
- **AC2** (`cell_cm:5`, толстые стены ≥12.5 см — поведение не меняется).
Проверено арифметикой на границе: `wallThickHoverHalfUnits(12.5, 5,
GRID_PITCH) === GRID_PITCH*1.25` — ровно то значение, которое давал старый
пол `max(depth/2, gridPitch*1.25)` на границе применимости пола (для
`cm≥12.5` при `cellCm=5` пол никогда не был активен, значит новая формула
без пола даёт то же самое для всего диапазона `cm≥12.5`). Тест в
`test/grid-scale.test.mjs` есть и проходит.
- **AC3** (`cell_cm:5`, стена 3 см → честные 2.5 юнита, не 10.42). Unit
`wallThickHoverHalfUnits(3, 5, GRID_PITCH) * 2 === 2.5` — проходит; это
именно тот случай, где старый пол раздувал тонкую стену, и мутант
`wallthick-hover-floor-back` подтверждает, что тест ловит возврат старого
поведения.
- **AC4** (стена `cm=0` — видимый минимум, физически одинаковый на любом
`cell_cm`, при `cell_cm:5` — байт-в-байт сегодняшний вид). Два unit-теста:
первый — `atFive*2 === GRID_PITCH*3` (это ровно старое `depth = gridPitch*3`
→ `half = 1.5*gridPitch` при отсутствии пола на `cellCm=5`, т.е. вид не
изменился); второй — пересчёт в физические сантиметры
`(half*2/gridPitch)*cellCm` даёт 15 см на обоих `cellCm=5` и `cellCm=30`,
подтверждая масштабную инвариантность через `gridVisualUnits` (та же
конвенция, что уже используют штрихи, `styles.ts:1549`). Мутант
`wallthick-zero-strip-not-visual` подтверждает, что тест реагирует именно на
потерю этой инвариантности.
- **AC5** (зона попадания курсора не сужена). `_wallThickHit` не входит в диф
этого диапазона (проверено чтением всего диффа) — `pull = gridPitch*6`
физически не менялся. Дополнительно подтверждено исполнением:
`generousHitAreaUnchanged` в смоке бьёт на расстоянии `gridPitch*5` от оси и
попадает; мутант `wallthick-hit-narrowed` (сужение до `gridPitch*2`) ловится
тем же смоком.
- **AC6** (мусорный ввод: `cm<0`/`NaN`/`Infinity` → как 0; `cellCm≤0`/`NaN` →
как 5; `gridPitch` невалиден → 0). Unit-тест перебирает `[-1, NaN, Infinity,
-Infinity]` для `cm` и `[0, -1, NaN, Infinity, -Infinity]` для `cellCm`,
плюс отдельно `cellCm=0` для стены с реальной толщиной
(`wallThickHoverHalfUnits(50, 0, GRID_PITCH) === 5*GRID_PITCH` — откат к
эталонной клетке 5 см) и `gridPitch=NaN`
(`wallThickHoverHalfUnits(50, 30, NaN) === 0`, поскольку `wallCmToUnits`
домножает на `gridPitch`). Все проходят; прочитана реализация — три
независимые `Number.isFinite`-проверки покрывают именно эти случаи, догадок
не найдено.
## Одно число — один источник
Полоса подсветки не дублирует показанное пользователю числовое значение
(диалог толщины показывает `cmToField(cm,…)`, полоса — только визуальная
ширина); проверка неприменима как «два места показывают одно число». Но по
существу дефект #303 был именно расхождением *визуальной* величины с телом
стены при одном общем источнике данных (`hit.cm`) — новая реализация устраняет
это, вызывая ту же `wallCmToUnits`, что и тело стены, а не независимую
формулу. `test/single-source-numbers.test.mjs` (не относится напрямую к этому
диапазону) прогнан — 3/3 green, регрессий не внесено.
## Соответствие ТЗ и его допущениям
ТЗ (лёгкий трек) уточнило формулировку исходного issue («минимум в
сантиметрах» → «grid-visual юниты, существующая конвенция проекта») и это
уточнение было принято зелёным ревью ТЗ. Реализация буквально следует
контракту §2 ТЗ: новая функция в точности с указанной сигнатурой и телом,
`_wallThickHover` не содержит собственной арифметики, `_wallThickHit`/стили
полосы не тронуты. Ни одного расхождения между ТЗ и кодом не найдено.
## Чего не проверял
- **Полный набор смоков (188 файлов)** — не запускал целиком; по
`scripts/smoke-select.mjs` диапазон даёт только «прямые совпадения» (4 файла,
все прогнаны и зелёные), «широких» символов нет. Диапазон меняет ровно один
вычисляемый геттер и одну чистую функцию без побочных эффектов — расширять
выборку не увидел оснований.
- **Полный прогон `mutation-gate.mjs` по всем ~40 мутантам** — по построению
скрипта это пред-релизный гейт (дорогая пересборка бандла на каждый
мутант); для этого диапазона проверил точечно три новых мутанта (все
ловятся) и применимость патчей (`--check`, green).
- **`npm run golden:verify` / golden capture** — не запускал: `wallthick-hover`
не участвует в `demo/golden/matrix.mjs` (grep пустой), полоса подсветки не
входит ни в одну golden-сцену, изменение не может дать диф golden по
построению.
- **`npm run invariants`** — не запускал: диапазон не пишет и не
трансформирует геометрические структуры (стены/`layout`/`marker.space`/
`open_spans`), только читает уже вычисленное `hit.cm` для отрисовки.
- **`python -m pytest tests_backend -q`** — не запускал: диапазон не трогает
ни одного `custom_components/**/*.py` файла (`git diff --stat` подтверждает).
- **Performance-профили** — не запускал: ни ТЗ, ни AC не называют
perf-чувствительный путь; изменение — замена одной арифметической
формулы в геттере подсветки, не затрагивает пути измерения перформанса.
- **`git diff --check` (whitespace)** — не запускал отдельно; не увидел его
необходимости, поскольку diff небольшой и был прочитан целиком построчно.
## Вердикт
**Зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 → в задаче.**
Все 6 AC ТЗ подтверждены — частично исполнением (целевой смок, точечные
unit-тесты, три точечных мутационных гварда), частично чтением там, где
исполнение избыточно (`_wallThickHit` не в диффе). Дешёвые гейты
(`typecheck`, `test`, `build`, синхронность бандла, `check-docs`) зелёные.
Единственная находка — Low, косметическая, не в коде и не требует правки.
Готово к очереди на пре-релиз (`S8-merged` по факту прохождения ревью,
согласно конвейеру).