mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
231 lines
20 KiB
Markdown
231 lines
20 KiB
Markdown
# 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` по факту прохождения ревью,
|
||
согласно конвейеру).
|