21 KiB
CODE-REVIEW-199-r1
- Issue: #199 — geometry preflight перед записью Optimize
- Спецификация:
docs/specs/199-optimize-geometry-preflight.md(зелёное ревью ТЗ:docs/reviews/SPEC-REVIEW-199-r2.md) - Коммит на ревью:
482afb73ebe483e316dcc1652f10736a2215c158(issue/199-optimize-geometry-preflight, диапазонorigin/dev...HEAD) - Заход: r1 · блокирующих циклов ревью кода израсходовано 0/4 (первый заход этого этапа — полный разбор, дельты нет)
Скоуп
Диапазон origin/dev...HEAD, 28 файлов. Продуктовый код: новый чистый модуль
src/plan-geometry-preflight.ts, точечная интеграция в src/houseplan-card.ts
(_openAlignDialog, _runAlignToGrid, _renderAlignDialog, извлечение общих
pure-хелперов geometryOpenCuts/geometryOpenPairs/geometryOpenings/… из
карточки в новый модуль), 4 новых i18n-ключа RU/EN. Backend
(custom_components/houseplan/**/*.py) не тронут — подтверждено diff'ом,
соответствует §9 ТЗ. Остальное — тесты (unit/smoke/benchmark/mutation),
golden-матрица (2 новых сценария, без baseline), документация (CANVAS,
ARCHITECTURE, USER-GUIDE.ru, TESTING, STATUS, оба CHANGELOG), три копии
бандла и docs/images/screenshots.json (пересчитанный fingerprint).
Как проверялось
Гейты
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
green, без вывода |
| Unit | npm test |
1068/1068 pass, 0 fail, 0 skipped (у автора было 1067/1, здесь Chromium в окружении полный — расхождение не в мою пользу, не блокирует) |
| Build + 3 копии бандла | npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js |
green, обе копии byte-identical, git status после сборки чист — в дереве и так лежали актуальные копии |
| Docs fingerprint | node scripts/check-docs.mjs |
green (7 files, 10 external links) — diff трогает src/**, гейт обязателен |
| Targeted smoke (AC4–AC9) | node demo/smoke_optimize_geometry_preflight.mjs |
green, все 16 ассертов true |
| Targeted benchmark (AC11) | npm run benchmark:optimize-geometry-preflight |
см. находку ниже — относительный бюджет проходит, абсолютный — нет в этом окружении |
| Mutation gate (AC12), 4 новых id | node scripts/mutation-gate.mjs --id=optimize-preflight-bypassed / --id=optimize-preflight-active-space-only / --id=optimize-preflight-accepts-null / --id=optimize-preflight-renders-apply-on-failure |
все 4 «поймано 1 из 1» — тест реально краснеет на соответствующей поломке |
| Golden (визуально, без accept) | node demo/golden/run.mjs --mode=capture --scenario=optimize-preflight-dialog-dark-en и …-light-ru |
missing-baseline (ожидаемо, эталон не принимается на ревью); оба PNG открыты и проверены визуально — см. ниже |
Не прогонялось и почему: полный demo/smoke_*.mjs (163 смока — задача не
задевает поверхности за пределами Optimize-диалога и общих geometry-хелперов,
выборка по diff/AC уже покрывает изменённые вызовы); npm run golden:verify
(эталонов для двух новых сценариев ещё нет — приём эталона возможен только
через golden:accept -- --reviewed на полном Linux CI артефакте перед бетой,
не на код-ревью); python -m pytest tests_backend (диапазон не трогает
custom_components/**/*.py); прочие 5 из ~1050 мутантов реестра
(node scripts/mutation-gate.mjs --check подтвердил, что все патчи реестра
по-прежнему ложатся на текущий код — офлайн-проверка целостности реестра,
дорогой полный прогон всех мутантов не требуется для одной задачи).
Разбор по AC1–AC14
- AC1 (parity). Построил построчную таблицу вызовов: карточка вызывает
wallBodiesGeometry(rooms, walls, openCuts, roomOpenings, wallKeyPitch, cellCm, gridPitch, coordScale, extras)в двух местах (houseplan-card.ts:5217ISO-рендер,:15641light-barrier) иfloorFootprintGeometry(rooms, walls, openCuts, wallKeyPitch, cellCm, gridPitch, coordScale)(:5224);plan-geometry-preflight.tsвызывает обе функции с теми же аргументами в том же порядке черезprepareSpacePhysicalGeometryInputs, который переиспользует вынесенные из карточки чистые хелперы (geometryOpenCuts/-Pairs/-Openings/- RoomOpeningInputs/-PartitionOpeningCuts) — карточка теперь тоже вызывает эти же экспортированные функции (diffhouseplan-card.tsзаменяет старые приватные копии на вызовы нового модуля), то есть renderer и preflight буквально сходятся на одном коде, а не на двух похожих реализациях. Вход не мутируется — проверено unit-тестом (assert.deepEqual(config, before)) и чтением:checkOptimizeGeometryтолько читаетconfig.spaces, не пишет в них. Проверено чтением кода + unit. - AC2. Параметризованный unit (
covers the production input matrix…) прогоняет ровно эту матрицу — room+wall, hosted opening, partitions/drafts/ columns без комнат, image-only, empty, floor-only — и ожидаетok/not-applicableбез failure. Green. - AC3. Unit
null, exceptions and floor failure are bounded…инжектитwallPass/floorPass/prepareSpace, возвращающиеnullи throw, и отдельно проверяет, что успешный пустой{geom: [], paperGeom: [], …}не считается failure (floorPass в этом случае намеренно бросаетmust not run— то есть тест доказывает, что приpaperGeomне-null floor вообще не вызывается). Exception text не протекает наружу —assert.doesNotMatch(…, /secret/)и аналогично дляprivate floor detail/private preparation detail. Green. - AC4. Unit
one failed space blocks the ordered whole-plan result…: третье из трёх пространств с id''фейлится через floor-null, сохраняется порядокconfig.spaces; смок проверяет то же на уровне карточки (0 WS-вызовов, диалог без Apply). Green. - AC5. В коде preflight нет ни одной ветки, читающей «состояние до
Optimize»:
checkOptimizeGeometryполучает толькоOptimizeResult.config(итоговый candidate) и не сравнивает его с исходным_serverCfg. Механизма исключения «не стало хуже» физически не существует — любой failing candidate блокируется вне зависимости от того, был ли он уже сломан до операции. Проверено чтением, не исполнением (в ТЗ AC5 назван «before/after regression unit + smoke» как способ доказательства; отдельного теста с таким названием нет, но отсутствие самой ветки логики делает регресс невозможным без явного добавления новой ветки — что уже поймает существующие unit'ы на failure-путях. Low, не поднимаю отдельной находкой: доказательство через чтение здесь надёжнее, чем тест на негативный случай, у которого нет кода для проверки). - AC6. Смок проверяет точный RU/EN текст сообщения и hint'а, XSS-пробу
(
<img id="preflight-injection">в title четвёртого пространства — не попадает в DOM и в textContent), отсутствие.btn.on. Золотые кандидаты (см. ниже) подтверждают то же визуально в обеих темах. - AC7. Смок:
noOpSkipsPreflight(0 доп. вызовов_checkOptimizeGeometry,preflight === null) иnoOpKeepsExistingUi(старый текстgs.align_none, без Apply);greenPreviewOffersApply— green-кандидат сохраняет Apply. - AC8. Смок явно считает вызовы
_checkOptimizeGeometry(checkscounter): unchanged Apply не увеличивает счётчик (unchangedApplyDoesNotRecheck), изменениеd.config.spaces[0].titleпосле preview увеличивает его на 1 и переводит диалог в fail-closed (changedFingerprintRechecks,changedFingerprintFailsClosed). - AC9. Смок перехватывает
hass.callWSи считает вызовыhouseplan/plan/optimize: 0 при красном (redPreflightMakesZeroWrites) + полное сохранение_serverCfg/_layout/revisions/undo-state (redPreflightPreservesAllState); ровно 1 atomic-вызов при зелёном с точным candidate (greenApplyMakesOneAtomicWrite, сверкаJSON.stringifyкандидата и отправленного payload). - AC10. Чтением:
_runAlignToGridпосле нового guard'а и fingerprint- проверки не тронут — тот же WS-вызов, тот же catch наe.code === 'conflict', та же запись revisions/undo (см.houseplan-card.ts:14917–14952, вне diff'а). Backend не менялся (нет diff'а вcustom_components/**/*.py). Смок дополнительно подтверждаетgreenApplyPreservesUndoContract. - AC11. См. отдельную находку ниже — относительный бюджет выполнен,
абсолютный нестабилен в этом CI-окружении. Cache: preflight-результат живёт
только в
_alignDialog.preflight(плейн-поле инстанса), отдельной Map/WeakMap-подобной структуры модуль не заводит — закрытие диалога (_alignDialog = null) делает результат недостижимым. Проверено чтением. ВызовыcheckOptimizeGeometry/_checkOptimizeGeometryесть только в_openAlignDialog(гейтированоr.changed) и в fingerprint-mismatch ветке_runAlignToGrid— не вrender()/updated()/pointer-путях. Проверено чтением. - AC12. 4 mutation-id из ТЗ (
optimize-preflight-bypassed,-active-space-only,-accepts-null,-renders-apply-on-failure) есть вscripts/mutation-gate.mjs; каждый прогнан лично (не со слов автора) — «поймано 1 из 1» на всех четырёх. - AC13.
check-docsgreen; три копии бандла byte-identical; RU/EN changelog,USER-GUIDE.ru.md,CANVAS.md,ARCHITECTURE.md,TESTING.md,STATUS.mdобновлены по существу и без придуманной терминологии (термин «пространство» уже используется вUSER-GUIDE.ru.mdдля Optimize-контекста). - AC14. См. таблицу гейтов выше — все green, кроме отмеченной находки по AC11/§10, которая по тексту самого ТЗ не блокирует эту задачу.
Находки
Low / информационная — абсолютный p95-бюджет §10 нестабилен в CI-песочнице (не блокирует)
npm run benchmark:optimize-geometry-preflight дважды подряд на коммите
482afb7 в этом окружении (контейнер код-ревью, 4 vCPU): baseline (прямой
production-вызов, БЕЗ preflight-обёртки) p95 ≈ 283–285 ms — уже выше
абсолютного порога 250 ms сам по себе; candidate (полный preflight) p95 ≈
291–296 ms. Относительный оверхед обёртки ≈ 3–4%, далеко внутри допуска
+20%/+15 ms (relativeLimitP95Ms 354–357 ms, оба раза с большим запасом).
Автор на своей машине получил baseline 165.98 ms / candidate 171.58 ms — то же
относительное соотношение, другая абсолютная база. Это ровно случай,
предусмотренный §10 ТЗ дословно: «Если абсолютный budget нестабилен в CI,
ревьюер… заводит отдельный performance issue; для #199 остаётся обязательным
относительный budget» — относительный бюджет выполнен, поэтому AC11 в части
#199 закрыт; абсолютная цифра — предмет отдельного трекинга.
Заведено: #240
(tech-debt, P3, S1-new), со ссылкой на #199 и точными числами обоих прогонов.
Не в скоупе #199 и не блокирует его вердикт — это единственная находка обзора,
и она не про поведение продукта, а про калибровку бенчмарка под CI-раннер;
скрипт также нигде не подключён к validate.yml/performance.yml, поэтому
регресс сейчас не может уронить CI молча — это тоже отмечено в заведённом
issue как отдельный открытый вопрос.
High-находок нет. Других Medium/Low — нет.
Что проверено и корректно
- Единый источник geometry-инпутов: карточка и preflight используют одни и те
же экспортированные pure-хелперы (
geometryOpenCuts,geometryOpenPairs,geometryOpenings,geometryPartitionOpeningCuts,geometryRoomOpeningInputs,prepareSpacePhysicalGeometryInputs) — diffhouseplan-card.tsзаменяет прежние приватные копии условий на вызовы того же модуля, а не дублирует логику рядом. - Различение structural failure vs successful-empty:
wallBodiesUnionPath()(существующий production-путь,wall-thickness.ts:2043) отдельно документирует «successful empty result: do not resurrect raw rings» — то же различение (united == null= failure,united.geomпустой = ok) сделано в preflight тем же способом, не новым отдельным алгоритмом. not-applicableдля комнат-less/wall-less пространства и физический-only путь (partitions/drafts/columns без комнат, floor не проверяется) — соответствуют §7.3 ТЗ и подтверждены unit-тестом.- Fail-closed UI-контракт: кнопка «Оптимизировать» физически не рендерится при
failure (
nothing, неdisabled), что закрыто mutation-тестомoptimize-preflight-renders-apply-on-failure. - Экранирование имён пространств (Lit text binding) — подтверждено смоком с
инъекцией
<img>в title. - RU/EN текст сообщения/hint'а — дословное совпадение с §8.1 ТЗ, проверено и строкой в смоке, и визуально на golden-кандидатах (dark/EN, light/RU — открыты и прочитаны лично, рендерятся корректно, без Apply, с ожидаемым текстом).
- Backend не тронут — permission/schema/revision/atomicity-барьеры остаются
прежними; preflight явно не заявлен как security-attestation (§9
ТЗ/
ARCHITECTURE.md). - i18n-ключи присутствуют в обоих
src/i18n/en.jsonиsrc/i18n/ru.json. - Три копии бандла синхронны,
check-docsзелёный,docs/specs/README.mdдополнен строкой на #199.
Чего не проверял (и почему)
- Полный
demo/smoke_*.mjs(163 файла) иnpm run golden:verify— вне соразмерного этой задаче объёма (PROCESS.md §8): diff не задевает поверхности за пределами Optimize-диалога и уже переиспользуемых geometry-хелперов; названные в AC/задетые смоки (smoke_optimize_geometry_ preflight, плюс упомянутые авторомsmoke_optimize_coordinate_ canonicalizationиsmoke_optimize_micro_intervalкак соседние по тому же диалогу — не перепроверял их лично, положился на зелёныйnpm test/typecheck и то, что diff их не касается) прогнаны точечно. python -m pytest tests_backend— диапазон не содержит изменений вcustom_components/**/*.py.- Полный прогон
scripts/mutation-gate.mjsбез--id(~90 мутантов, дорогая пересборка бандла на каждый) — это пре-релизный гейт; проверил офлайн целостность реестра (--check, все патчи ложатся) и лично прогнал 4 новых id, относящихся к #199. - Принятие golden-эталонов — не в полномочиях код-ревью (только
golden:accept -- --reviewedна полном Linux CI артефакте перед бетой); визуально проверил оба capture-PNG вместо этого. - Performance-профили за пределами названного в AC11 large-house бенчмарка —
не требуются: задача не касается render/pointer/HA-tick путей ни в скоупе, ни
по факту (проверено чтением вызовов
checkOptimizeGeometry).
Итог
Реализация соответствует контракту ТЗ #199 построчно: единый источник production-geometry, fail-closed whole-plan barrier, различение failure/ not-applicable/successful-empty, fingerprint-повторная проверка, RU/EN UX без утечки технических деталей, backend не тронут, mutation-покрытие AC12 лично перепроверено и действительно ловит поломки. Единственная находка — информационная нестабильность абсолютного perf-бюджета в CI-окружении, предусмотренная самим ТЗ и не блокирующая эту задачу; заведена отдельным issue #240.
Вердикт: зелёный.