19 KiB
CODE-REVIEW-676-r1
Материал ревью: dd905888cbf016a76457b1c17e2bb8d57143645f (ветка issue/676-stairs-editing,
git log --oneline origin/dev..HEAD: 93051edf fix · 89e8aecb refactor ·
69e03451 chore(golden) · ab89187f test(bundle) · dd905888 test(mutation)).
Заход r1, полный разбор (первый код-ревью этого этапа; предыдущие пять
комментариев «ревью не запускалось» цикл не тратили — код никто не читал).
Скоуп
Слой редактирования лестниц (src/stairs-editor.ts) переписан на
box-контракт декора/мебели: чистые функции протяжки/ресайза/магнита/курсора/
диалога вынесены в новый ленивый модуль src/stairs-box.ts; рамка выделения
перенесена в верхний оверлей карточки (src/houseplan-card.ts, +2 строки);
подсказка Просмотра — в src/stairs-view.ts; стили — src/styles/plan.styles.ts;
i18n ×4; golden stairs-flat-selected-light принят; тесты — новый
test/stairs-box.test.mjs, расширенный demo/smoke_stairs.mjs, шесть новых
мутантов + обновление одного старого + два переведённых в browser-offload
witness; попутный B-фикс test/bundle-assets.test.mjs (потолок графа судится
только по свежему бандлу, #657). Класс A+B+C+D, продуктовая задача, полный
трек — соответствует заявке автора.
Закрывает J6 (docs/SCOPE.md: «Keep the plan true as the home evolves» —
редактор плана) и, подсказкой в Просмотре, J1 (пространственный обзор дома).
Девять исходных дефектов (#663) сведены к контрактам К1–К8 и покрыты AC1–AC12.
Как проверялось
Дешёвые гейты подтверждены зелёным Validate на этом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/36328481049,
conclusion: success, задачи Фронтенд: типы, юниты, мутанты, синхрон бандла
и все шесть шардов «Мутанты по диффу» — success); тяжёлые джобы (golden,
smoke, performance_smoke, backend, hassfest, hacs, geometry_parity)
в этом прогоне skipped (не heavy-триггер) — они не покрыты и перепроверены
мной локально ниже.
| Гейт | Прогнан | Результат |
|---|---|---|
npx tsc --noEmit / npm run build |
через Validate (SHA) + локально npm run bundle:sync |
зелёный |
npm test |
через Validate (SHA) + локально npm run gate:small («юниты (npm test)» 70 с) |
зелёный |
node --test test/stairs-box.test.mjs (с tsc test-build) |
локально | 12/12 pass |
npm run gate:small |
локально, полностью | зелёный (build+typecheck, no-new-any, no-new-private-writes, smoke-select, юниты, bundle-policy --verify, бюджет, lint:unused — все ok) |
node demo/smoke_stairs.mjs (AC1, AC4–AC8, AC11, AC12) |
локально, свежий бандл (bundle:sync) |
55/55 проверок true, OK |
npm run golden:verify |
локально, свежий бандл | 184/184 passed, включая stairs-flat-selected-light |
Мутанты задачи (stairs-handle-cursor-ignores-bearing, stairs-dialog-clamps-to-wall-thickness, stairs-resize-mirrors-past-anchor, stairs-move-magnet-turns-any-angle, stairs-gesture-click-reaches-plan-tool, stairs-view-tooltip-ignores-target-state) + обновлённый stairs-link-auto-creates-target-object |
node scripts/mutation-gate.mjs --id=<каждый> |
7/7 «заявленный тест покраснел на мутанте» |
node scripts/mutation-gate.mjs --check |
локально | browser guards: 200/200; 3 предупреждения реестра — все #650 (шаблонные имена тестов в geometry-corpus.test.mjs/mutation-nightly-reuse.test.mjs), не относятся к этой задаче |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
локально | 68 прямых совпадений, все по общим идентификаторам (_mode, _model, _curSpaceCfg, stopPropagation, cellCm, _editorRuntime…), кроме demo/smoke_stairs.mjs — единственного смока, реально завязанного на изменённый код |
Спот-чек смоков из выборки (smoke_furniture, smoke_furniture_polish, smoke_decor, smoke_wall_chain_thickness, smoke_room_resize) |
локально | все OK |
Чего не проверял и почему
- Остальные ~135 смоков из широкой выборки
smoke-selectне прогонял: фактический продуктовый диф сужен доstairs-editor.ts,stairs-box.ts(новый, импортируется только ленивым редактором),stairs-view.ts,stairs-editor-model.ts(2 строки — комментарий),plan.styles.ts(только классы лестниц + одна аддитивная строка к существующему правилу.dtfurnitureframe .dtrot) иhouseplan-card.ts(+2 строки, оба под_mode === 'plan'и_tool === 'stairs'/наличие_editorRuntime). Совпадения инструмента — по общеупотребимым идентификаторам, не по специфике правки; спот-чек пяти наиболее смежных (мебель, декор, магнит стены, ресайз комнаты) прошёл зелено. python -m pytest tests_backend— не прогонял: диф не касаетсяcustom_components/**/*.py.npm run invariants— не прогонял: гейт проверяет ссылки/геометрию стен, перегородок иwall_keys/hidden_obstacles/physical_geometry; модельstairsи её валидация — заявленный и подтверждённый чтением не-скоуп,stairs.tsне тронут. Изменилась только редакторская box-математика над уже существующими полямиx/y/angle/length/width/radius, которые инвариант-гейт не проверяет.- Полная матрица
golden/smoke/performance_smoke(все 278 браузерных смоков) — предрелизная обязанность, не гейт код-ревью; прогнан AC-набор и прямое совпадение (smoke_stairs.mjs) плюс точечная выборка. - Отпечаток скриншотов документации (
check-docs --screenshots=strict) — подтверждённо устарел (warn на push, error только на кандидате); автор прямо перечислил это в «НЕ сделано» со ссылкой на прецедент #479 — корректно, не блокирует ревью.
Находки
Нет ни одной блокирующей (High) или требующей возврата (Medium в скоупе) находки.
Low (снята мной, с записью — не блокирует, автор менять не обязан).
src/stairs-editor.ts:305, saveDialog: сравнение «ничего не изменилось»
берёт current.target_space_id ?? null для эталона, а вычисляет
dialog.targetSpaceId || null для нового значения. Если бы в конфиге когда-
либо оказалась пустая строка target_space_id: '' (модель это допускает —
stairs.ts:84 требует только string | null), «Сохранить» без единой правки
поля превратило бы '' в null и всё равно записало бы историю — минимальное
нарушение К7 «ничего не пишет». Не воспроизводится через сам редактор: каждый
путь записи (defaultStair, draftStair, saveDialog) уже отдаёт только
null или непустую строку, значит '' может попасть в конфиг только через
ручное редактирование YAML/импорт в обход UI. Не блокирую: сценарий не
описан ни одним AC, а починка ради теоретического края потребовала бы либо
менять модель (не-скоуп задачи), либо использовать несимметричную проверку
только в редакторе — стоимость несоразмерна вероятности.
Проверено и корректно (по контрактам К1–К8)
- К1/AC1 — доминирующая ось протяжки, tie-break по x, знаковые углы
0/90/180/270,
direction: forward, минимум 30 см по обеим осям, винтовая — квадрат от A к B; клик короче клетки — дефолт с магнитом перемещения. Юнитыtest/stairs-box.test.mjs(draftStair,draftLeadingHandle) плюс смок (dragDrawsTheDrawnSize,dragUpwardRisesUpward,dragDrawsTheSpiralSquare) — реальные числовые проверки размеров/центра/угла, не пустые true. - К2/AC4/AC7 — рамка в
<g data-hp="stair-frame">вставляется картой в верхний оверлей после_renderTextFrame, выше.wallbodies(compareDocumentPositionв смоке подтверждаетDOCUMENT_POSITION_FOLLOWING); радиус узла —0.018 × max(view.w, view.h), постоянен на экране (проверено на зуме ×1 и ×3 в смоке, различие ≤ 1 px); курсор по сектору мировой нормали (resizeCursor) покрыт юнитами на 0/45/90/135/180/225/270/315°, на повёрнутых на 10/30/90° лестницах, плюс смок на реально повёрнутом узле черезgetComputedStyle. - К3/AC3 —
snapEdgeToFaces/magnetStairMove: сторона в пределах 6 клеток и 5° ложится на грань; поворот ограничен 5° (юнит проверяет ровно «повернулось до параллели, не дальше»); 30°-лестница не трогается; грань, смотрящая внутрь массива (скрытая в теле стены), не магнитит — юнит с двумя вариантами обхода полигона (по часовой/против) плюс отдельный тест на лестницу, перекрывающую тело стены снаружи/изнутри. - К4/AC2/AC11 —
resizeStair: неподвижный якорь проверен геометрически (совпадение середины неподвижной стороны до/после с точностью 1e-9), угол не меняется, протаскивание за якорь клэмпится к минимуму без зеркала,Shift— пропорционально по большей оси. Отдельный юнит на винтовой (растёт один тангенс, противоположный неподвижен). - К5 — вращение продолжает существующий контракт (не менялось по ТЗ),
проверено смоком (
shiftRotationSnaps45). - К6/AC6 —
_suppressClick/swallowNextClickплюс@click=${stop}на обоих видах узлов; смок проверяет и «Лестницу» (resizeUnderStairsToolAddsNoStair,rotateUnderStairsToolAddsNoStair), и «Выбор» (resizeUnderSelectKeepsSelection), с реальным синтезированнымclickпослеpointerup, как это делает браузер. - К7/AC5 —
stairFieldToCm/stairSizeFromField: границы 30–10000 см, 424,62 × 155,38 см сохраняются бит в бит (юнит plus смокuntouchedDialogSaveKeepsSizes+untouchedDialogSaveWritesNoHistory, сравнениеJSON.stringifyдо/после и размера_geometryHistory). - К8/AC8 —
stairTargetState === 'active'как единственное условие подсказки; все пять состояний (active/missing/self/deleted/fixed) проверены отдельными смоук-шагами (hoverTip) с реальными pointerenter/pointerleave и проверкойcard._tip. - AC9 — golden
stairs-flat-selected-lightпринят из полного Validate (run 36320878644) после ребейза с тем же отпечатком исходников; локальная перепроверка (golden:verify, свежий бандл) — 184/184passed, остальные три лестничные сцены не изменились. - AC10 — i18n-паритет en/ru/de/fr подтверждён диффом (
stairs.tooltip_navigate,history.stair_resize,history.stair_rotate, идентичные плейсхолдеры) и зелёнымtest/i18n.test.mjsвnpm test. - AC12 — переключение на «Стены» убирает
.hp-stair-frameиз DOM (_activateMarkupTool:if (tool !== 'select' && tool !== 'stairs') this.stairs.clearSelection(), плюсinputEnabledуже гейтитrenderFrame); клик по точке бывшего узла двигает_path.length(смок:frameGoneUnderWallsTool,wallsToolClickIsAWallPoint,frameReturnsUnderSelect). - Бюджет: eager-код лестниц вынесен в
stairs-box.ts(импортируется только ленивым редактором) вторым коммитом именно потому, что первая версия съела ~2,5 КБ initial View; итоговый запас initial View 1 896–1 973 Б (уже был тесен до ветки — падение на 77 Б, не новое сужение), потолок ленивого редактора поднят в том же коммите, что и график (bundle-budget.mjs,core-file-budget.test.mjs,monolith-baseline.json— числа сверены и совпадают с заявленными). - Мутанты
stairs-gesture-click-reaches-plan-toolиstairs-view-tooltip-ignores-target-stateпереведены в browser-offload witness (test/mutation-browser-offload.test.mjs) по прецеденту #659 — это не файл монолита (houseplan-card.ts/houseplan-editor-runtime.ts), freeze-списокtest/monolith-text-anchors.test.mjsне расширен, правило §2.7 о контрактах по монолиту не задето. Витнесс перепроверен вручную: обе строковые проверки (наличие точного блока подавления клика, счётчик@click=${stop}= 2, отсутствие прямого_showTipбез гварда) действительно краснеют на соответствующих мутантах — не ритуальная регулярка. - Документация (
docs/STAIRS.md,docs/ARCHITECTURE.md,USER-GUIDE(.ru).md,docs/data-hp-contract.json) описывает ровно реализованное поведение, терминология («рамка», «узел», «магнит», «Переход на этаж …») совпадает сdocs/USER-GUIDE.ru.md. Оба changelog правлены в том же коммите, что иUser-Visible: yes(93051edf); одно число, видимое пользователю («Переход на этаж {title}»), — источник один:stairTargetState+_model.find(...).title, использован и подсказкой, и (без изменений) навигацией вstairs-view.ts. - Трейлеры: каждый коммит несёт
Issue: #676;User-Visible: yesтолько на93051edf(с обоими changelog);89e8aecb/ab89187f/dd905888—User-Visible: no, корректно (рефакторинг, тестовый гейт, тестовые свидетели);69e03451несётRelease:/Baseline-Reviewed:за правкуdemo/golden/baselines/**, как того требуетAGENTS.md.
Риски (приняты, не находки)
- Перф жеста (
write()на каждыйpointermove) — вне скоупа, унаследовано от #663, задокументировано автором. _suppressClickсгорает поsetTimeout(0); если браузер не синтезируетclick(отменённый touch), флаг просто не используется — без побочных эффектов, автор отметил это явно.- Обход тел стен для магнита берёт сторону из знака площади полигона; самопересекающиеся тела не встречались и не тестировались — принято как документированное ограничение, симметричное существующему магниту мебели.
Материал раунда
SHA: dd905888cbf016a76457b1c17e2bb8d57143645f.
Предыдущего раунда код-ревью для этой задачи не было (пять комментариев
«Ревью не запускалось» — красный/несостоявшийся Validate и обрыв
model-review, ни один цикл ревью не потрачен). Разделы «Закрытие раунда
r0» и «Унаследовано» не применяются.
Материал раунда
- Ветка:
issue/676-stairs-editing, коммитdd905888cbf0— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
799688ce253fd14dd4ebacdb6e4166243dde9431git log --all --format='%H %T' | grep 799688ce253f - Тело issue:
72bccdffa1d7cdfdd71c8c8ee5d1cd66f64b6aba7db2003b718c0af2f2292183 - Вердикт конвейера:
green· High 0