Files
houseplan-card/docs/reviews/CODE-REVIEW-223-r1.md
T
2026-08-20 19:32:16 +00:00

167 lines
17 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.
# Код-ревью #223 — Optimize канонизирует координаты без floating-point шума
- Цикл: r1/4 (код-ревью; ТЗ прошло отдельный лимит и закрыто зелёным на r3)
- Диапазон: `git diff origin/dev...HEAD` (7 коммитов, `origin/dev..HEAD`)
- Реализация: коммит `38f74e0` (`fix: canonicalize near-grid coordinates exactly`,
`Issue: #223`, `User-Visible: yes`) — единственный коммит с продуктовым
изменением; предыдущие 6 коммитов — ТЗ и его ревью (r1–r3), уже закрыты
отдельным циклом.
- ТЗ: `docs/specs/223-optimize-coordinate-canonicalization.md`, зелёное ревью
`docs/reviews/SPEC-REVIEW-223-r3.md`.
## Скоуп проверки
Полный разбор — это первый цикл код-ревью, §2.10 (дельта по раундам) здесь не
применяется. Список изменённых файлов (`git diff origin/dev...HEAD --name-only`)
сверен построчно с §4/§12 ТЗ — расхождений нет, попутных правок нет:
`src/align-grid.ts`, `src/houseplan-card.ts`, `src/i18n/{en,ru}.json`,
`test/align-grid.test.mjs`, `test/plan-optimizer.test.mjs`, `test/i18n.test.mjs`,
`scripts/mutation-gate.mjs`, `demo/smoke_optimize_coordinate_canonicalization.mjs`,
`docs/{CANVAS,USER-GUIDE.ru,TESTING,STATUS,CHANGELOG,CHANGELOG.ru}.md`,
`docs/specs/README.md`, три копии бандла. `custom_components/**/*.py` не
тронут — контракт ТЗ («persisted schema и backend не меняются») подтверждён
структурой diff, не только текстом.
## Как проверялось (гейты)
Прогнано в этом цикле:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | pass, без вывода |
| Unit | `npm test` | `tests 975, pass 975, fail 0` |
| Build + сверка бандлов | `npm run build` затем `sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js demo/srv/assets/houseplan-card.js` | все три файла — один и тот же хэш `3f7eaf824d79b33f190b9323e2d873aff7eb19d6eef7749488e96b39c76ad33a` (совпадает с хэшем из хендоффа автора) |
| Targeted browser smoke (AC6/AC7) | `node demo/smoke_optimize_coordinate_canonicalization.mjs` | `13/13 true`, `OK` |
| Смежные browser smoke (диф трогает общий `alignAllToGrid`/`snapN`, используемый этими сценариями) | `node demo/smoke_grid_snap.mjs` и `node demo/smoke_optimize_micro_interval.mjs` | оба `OK`, все проверки `true` |
| Mutation guard (AC1) | `node scripts/mutation-gate.mjs --id=snapn-returns-input-near-node` | `поймано 1 из 1` — мутант пойман, чистый прогон зелёный |
| Mutation registry hygiene | `node scripts/mutation-gate.mjs --check` | `ok snapn-returns-input-near-node` среди прочих, без ошибок уникальности `find` |
| Process gate (офлайн) | `node scripts/process-gate.mjs` | `гейт пройден, предупреждений 0` |
Не прогонялось и почему:
- **Полный набор из 127 браузерных смоков** — diff меняет ровно один
batch-путь (`alignAllToGrid` внутри explicit Optimize) и явно поименованный
смок плюс два смежных по тому же пути прогнаны выше; остальные 124 смока не
затрагивают ни `align-grid.ts`, ни отчёт Optimize. Полный набор — предрелизный
гейт (PROCESS.md §8), не гейт ревью.
- **`npm run golden:verify`** — diff не меняет разметку диалога (тот же
`.alignmsg`, добавлена подстрока в уже существующий текст) и не двигает
геометрию видимо: величина правки — доли `EPS` (≈4.2e-9 нормализованной
единицы), она гарантированно ниже порога любого golden-снапшота. Само ТЗ
§10 фиксирует это решение («Golden baseline не требуется... перед бетой
выполняется общий golden verify по процессу»), и я согласен с этой оценкой
после чтения diff.
- **`python -m pytest tests_backend`** — ни один файл
`custom_components/houseplan/**/*.py` не тронут (подтверждено списком файлов
выше).
- **performance-профили** — в AC не названы; изменение линейно по уже
обходимым координатам, добавляет одну точную проверку на компонент; ТЗ §11
явно снимает необходимость отдельного performance-гейта, и чтение
`alignAllToGrid` (без новых циклов/аллокаций сверх O(1) на компоненту)
подтверждает это.
## Находки
Находок, блокирующих или требующих правки в задаче, нет.
### Low — AC2 (неконечные числа) не имеет отдельного unit-доказательства
- **Файл:** `src/align-grid.ts:69-72` (`snapN`), контракт — AC2 в
`docs/specs/223-optimize-coordinate-canonicalization.md:147`.
- **Что не так:** AC2 требует доказательства «boundary unit matrix» для двух
утверждений: (1) значение дальше `EPS` продолжает выравниваться как раньше —
это покрыто (`test/align-grid.test.mjs:48-50`, случай `off = node + S/3`);
(2) «не конечные числа сохраняют прежний результат» — для этого в текущем
наборе тестов нет ни одного `assert` на `NaN`/`Infinity`/`-Infinity`, ни в
новых тестах, ни в уже существовавших (`git show origin/dev:test/align-grid.test.mjs`
такого теста тоже не содержит).
- **Почему не блокирует:** строка `if (!Number.isFinite(v)) return v;`
(`src/align-grid.ts:70`) в этом diff не менялась — это тот же guard, что был
до задачи, и он был непокрыт тестом и раньше. Риск регрессии от этого diff
нулевой: изменённая строка (`return Math.round(...)`) находится строго после
guard и не может исполниться для нефинитного входа.
- **Решение ревьюера:** снимается без правки в этой задаче. AC2 по факту
доказан читкой (guard не тронут), а не только тестом; отдельная задача на
добавление такого unit-теста не заводится (тривиальная гигиена покрытия,
не дефект поведения) — по желанию автора может быть добавлена заодно со
следующей правкой этого файла.
## Проверено по AC (код-ревью отвечает за «оно вообще работает»)
| AC | Доказательство | Как проверено |
|---|---|---|
| AC1 | `test/align-grid.test.mjs:41-51` (юнит), мутант `snapn-returns-input-near-node` | Юнит прогнан зелёным в составе `npm test`; мутант отдельно прогнан и **пойман** (`scripts/mutation-gate.mjs --id=...` → `1 из 1`) — тест умеет падать. |
| AC2 | Юнит на `off`-случай зелёный; неконечные числа — см. находку Low выше | Частично тестом, частично чтением (guard не тронут). |
| AC3 | `test/plan-optimizer.test.mjs:75-107` (six-room ULP fixture), `unionBodies` | Юнит зелёный в `npm test`; проверено, что тест реально утверждает `moved:0, maxShift:0, coordsCanonicalized>0, changed:true` и что `unionBodies` не падает на результате — тест умеет падать (до фикса `snapN` возвращал `v`, координаты остались бы носящими шум, `coordsCanonicalized` был бы `0`, `assert.ok(... > 0)` покраснел бы). |
| AC4 | `test/align-grid.test.mjs:53-94` (poly/rect/partition accepted+rejected/wall_column/decor/marker) | Юнит зелёный; читкой кода (`src/align-grid.ts:264-291`) подтверждено, что вклад партиции считается **после** ветвления `hostedFit`, а не на входе в `snapN` — ровно то, что требовал r1 ревью ТЗ. Тест явно проверяет, что `partitions[1].a[0]` (`hostedFit=false`) остаётся `noisy`, а счётчик не растёт от неё (`coordsCanonicalized === 8`, вручную пересчитано по фикстуре — сходится). |
| AC5 | `test/align-grid.test.mjs:158-162`, `test/plan-optimizer.test.mjs:101-106` | Юниты зелёные, оба проверяют `coordsCanonicalized: 0` и `changed: false` на повторном проходе, включая глубокое равенство. |
| AC6 | `test/i18n.test.mjs:30-40`, targeted smoke | i18n-юнит проверяет ровно обе строки RU/EN и обе точки использования в `houseplan-card.ts` через regex по исходнику; smoke проверяет реальный рендер (`previewNamesBothReportUnits`, `toastExplainsZeroMoveCleanup`) — оба зелёные при прогоне выше. |
| AC7 | `test/plan-optimizer.test.mjs:83` (`assert.deepEqual(config, before, ...)`), smoke | Юнит на немутацию preview зелёный; smoke подтверждает Cancel/Apply/Undo на реальном диалоге (`previewDoesNotWrite`, `cancelDoesNotWrite`, `applyUsesOneAtomicWrite`, `undoRestoresExactNoisyValues`, `undoIsOneDeep`) — все `true` в прогоне выше. |
| AC8 | Чтение: `grep -rn "alignAllToGrid\|snapN(" src/*.ts` | `alignAllToGrid` вызывается ровно из одного места — `plan-optimizer.ts:398`, внутри `optimizePlans()`, который сам вызывается только явным `_runAlignToGrid`/`_openAlignDialog` (нет вызовов из пути save/render). Проверено чтением, не исполнением. Полный `npm test` (975/975) зелёный, включая существующие тесты #218. |
| AC9 | `git diff` по docs, `sha256sum` трёх бандлов | Документация обновлена по списку §12 ТЗ; хэши бандлов идентичны (см. таблицу гейтов). |
| AC10 | Таблица гейтов выше | typecheck/unit/build/targeted-smoke/mutation — все зелёные. |
## Что проверено и корректно (сверх таблицы AC)
- **Архитектура счётчика.** `noteCanonicalCoordinate`/`noteCanonicalPoint`
(`src/align-grid.ts:157-166`) считают вклад строго по значению, реально
записанному в candidate: для `room_drafts` вклад учитывается только для точек,
попавших в итоговый `draft.points` (`written` строится параллельно с `points`,
включая тот же dedup по `EPS`), и только когда сам draft не был отфильтрован
фильтром `points.length >= 2` (иначе весь draft уходит в `removedDrafts`, а не
в candidate). Для partition — вклад учитывается только внутри
`if (snappedLength > EPS && hostedFit)`, то есть ровно в ветке, где `p.a`/`p.b`
реально присваиваются.
- **Скрещённая проверка арифметики.** Вручную пересчитан фикстурный тест
`near-node report counts only coordinate values actually written to the
candidate` (`test/align-grid.test.mjs:53-88`): из восьми `noisy`-вхождений в
фикстуре ожидаемый вклад — poly (1: только x первой вершины), rect (1: x
дальнего угла через `x0+w0`), partition `accepted` (1), `wall_column` (1),
decor `line`/`box`/`text` (по 1), marker (1) = 8, ровно то, что проверяет
`assert.equal(result.report.coordsCanonicalized, 8)`. Партиция `rejected`
корректно исключена.
- **`OptimizeReport` наследует поле честно.** `plan-optimizer.ts:38` —
`OptimizeReport extends AlignReport`, а итоговый report собирается через
`{ ...alignReport, ... }` (`plan-optimizer.ts:522`) без явного поля —
`coordsCanonicalized` переносится автоматически, не задваивается и не
теряется; `changed` в `optimizePlans()` вычисляется отдельно, через полное
сравнение JSON конфигурации (`plan-optimizer.ts:513`), поэтому корректно
становится `true` даже если единственная правка — ULP-чистка.
- **`moved`/`maxShift*` не растут от ULP-правок.** Это гарантирует не новый, а
существующий `note()` (`src/align-grid.ts:181-187`, не изменён этим diff):
`moved` увеличивается только при `d > EPS`. Новый код добавляет отдельный
счётчик параллельно, не переиспользуя и не искажая этот путь.
- **UX-терминология.** Финальная строка `gs.optimize_changes` (RU: «обновлено
пространств: {c}; устранён шум координат: {p}»; EN аналогично) разводит два
счётчика без пересечения с занятым во всём проекте термином «нормализовано»/
«канонизировано» из `docs/CANVAS.md` — ровно то решение, которое было
согласовано на r2/r3 ревью ТЗ; я перечитал `docs/CANVAS.md:12,20,333,489,539`
и не нашёл нового конфликта термина с этой формулировкой.
- **Итоговый toast** (`gs.align_done`, `src/houseplan-card.ts:14288-14291`)
включает `coordsCanonicalized` в общую сумму обслуженных записей — предположение
§13.4 ТЗ реализовано буквально.
- **Отсутствие побочных путей записи.** Подтверждено чтением (см. AC8), что
`snapN`/`alignAllToGrid` недостижимы вне явного Optimize — обычные
read/render/Save не подвергаются риску скрытой мутации персистентных данных.
- **Три копии бандла синхронны**, коммит несёт верные трейлеры, оба changelog
правлены в том же коммите, что и поведение (`38f74e0`).
## Чего не проверял
- Полный browser-smoke набор (127 сценариев), `golden:verify`,
`pytest tests_backend`, performance-профили — не прогонялись; причины и
обоснование сужения — в таблице гейтов выше. Это решение ревьюера, а не
молчаливый пропуск.
- Ручного визуального прогона карточки в браузере (открыть демо, покликать
диалог глазами) не делал — заменён точечным browser-smoke и чтением
рендер-кода; в процессе фазы ручного тестирования нет (PROCESS.md §2.7), и
именно smoke здесь стоит на её месте.
- WSL/полный HA harness и Windows-специфичные smoke не запускались — сессия
ревью работает в Linux CI-подобном окружении, что и предусмотрено процессом
для код-ревью.
## Вердикт
Зелёный. High: 0. Medium: 0. Одна находка Low снята решением ревьюера с
записью (см. раздел «Находки») — правка не требуется.