25 KiB
CODE-REVIEW-304-r1
Issue: #304 — паритет базовых осей и узлов между инструментами Plan editor
Ветка: issue/304-plan-axis-parity, HEAD 7310ce0134ca8e5b1c32e6816a2e70625ea158e2
Диапазон: origin/dev...HEAD (2 коммита: a2b4d32b — код, 7310ce01 — скриншоты документации)
Трек: small (лёгкий), ТЗ в теле issue, ревью ТЗ — комментарий (зелёный, заход r1, 0/2 циклов, 2026-08-25T13:06:27Z)
Заход: r1 (первый код-ревью для этого issue; предыдущих раундов код-ревью нет, раздел «Унаследовано из r» неприменим)
Вердикт: жёлтый · High: 1 · Medium: 0
Скоуп
Контракт (тело issue): статический слой архитектурных осей/узлов (plan-snap-overlay)
должен рендериться во всех 10 MarkupTool, а не только в «Стены» (draw); transient
snap/hover остаются эксклюзивными для «Стены»; View/Device editor/Background editor слой
не получают.
Диапазон коснулся:
src/houseplan-card.ts— единственная продуктовая правка (класс A), 5 добавленных / 2 удалённые строки;demo/smoke_plan_snap_overlay.mjs,demo/golden/harness.mjs,test/plan-snap-overlay.test.mjs— тесты (класс B);docs/CHANGELOG.md,docs/CHANGELOG.ru.md,docs/STATUS.md,docs/USER-GUIDE.md,docs/USER-GUIDE.ru.md,docs/images/**,docs/images/screenshots.json— документация (класс C);dist/houseplan-card.js,custom_components/houseplan/frontend/houseplan-card.js— сгенерированное (класс D), синхронно сsrc.
demo/golden/matrix.mjs и src/plan-snap-overlay.ts не тронуты — геометрия и матрица
сцен не менялись, что соответствует заявленному «не-скоупу».
Сама правка
// было
${this._markup && this._tool === 'draw' ? svg`<g class="hp-editor-only-layer" …>
${this._renderPlanSnapOverlay()}</g>` : nothing}
…
private _renderPlanSnapOverlay(): TemplateResult {
if (!this._markup || this._tool !== 'draw') { return svg``; }
// стало
${this._markup ? svg`<g class="hp-editor-only-layer" …>
${this._renderPlanSnapOverlay()}</g>` : nothing}
…
private _renderPlanSnapOverlay(): TemplateResult {
if (!this._markup) { return svg``; }
Гейт _tool === 'draw' снят в двух местах; _activePlanSnapCandidate/_activePlanSnapConflicts
(строки 6996–7010, не изменены) по-прежнему возвращают null/[] вне draw — transient-маркер
и conflict-подсветка остаются эксклюзивными «Стенам», как требует контракт п.4. _markup
(строка 1524, не изменена) — это _mode === 'plan', поэтому View/devices/decor не получают
слой без дополнительных условий (контракт п.6).
Как проверялось
Дешёвые гейты — прогнаны все
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный, exit 0 |
| Unit | npm test |
1299 тестов, 1298 passed, 1 skipped (pre-existing #281 private-fixture skip, не связан с #304), 0 failed |
| Build + sync | npm run build && npm run bundle:sync |
зелёный; dist/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js, demo/srv/assets/houseplan-card.js byte-identical (cmp) |
| Docs fingerprint | node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 10 external links)» |
| Выбор смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
7 прямых совпадений по символу _markup; символ _tool инструмент не посчитал широким |
| Model invariants | — | не запускал: диф не трогает рёбра комнат, layout, marker.space, open_spans, записи толщины или plan-snap-overlay.ts — только условие рендера. Проверено чтением |
Дисциплина «тест умеет падать» — проверено активно, не на слово
Временно откатил src/houseplan-card.ts к состоянию origin/dev (git apply -R), пересобрал
бандл (npm run bundle:sync) и повторно прогнал:
node demo/smoke_plan_snap_overlay.mjs→ падает необработанным исключением (Cannot read properties of null (reading 'querySelectorAll')) на проверкеwallthick— без фиксаoverlay()для этого инструментаnull, ассерт паритета невыполним;node demo/golden/run.mjs --mode=capture --scenario=hidden-wall-diagnostics-plan-light→ статусerror(новая проверка паритета внутриprepareGoldenScenarioбросает исключение до снятия скриншота).
Затем восстановил фикс (git apply), пересобрал бандл — дерево вернулось к состоянию коммита
(git status пусто), оба прогона снова зелёные. Тест и golden-инвариант доказанно способны падать.
Смоки — прямые совпадения плюс релевантные по AC6, прогнаны лично
node demo/smoke_edit_walk.mjs OK
node demo/smoke_editor_gestures.mjs OK
node demo/smoke_merge_split.mjs OK
node demo/smoke_optimize_coincident_partition.mjs OK
node demo/smoke_resize_audit_1550.mjs OK
node demo/smoke_room_resize.mjs OK
node demo/smoke_split_nonsnap.mjs OK
node demo/smoke_plan_snap_overlay.mjs OK (все 43 подпроверки true)
node demo/smoke_wall_thickness.mjs OK
node demo/smoke_opening_preview.mjs OK
node demo/smoke_partition_openings.mjs OK
node demo/smoke_resize_wall_thickness.mjs OK
node demo/smoke_unified_wall_tool.mjs OK
node demo/smoke_wallthick_hover_width.mjs OK
Полный набор demo/smoke_*.mjs (188 файлов) не прогонялся — задача не задевает всё, только
рендер-слой Plan editor; полный набор остаётся предрелизным гейтом (§8).
Golden — локально плюс канонический Linux CI-артефакт с branch'а автора
Локально (native Windows, диагностически, per AGENTS.md «Известное окружение»):
node demo/golden/run.mjs --mode=capture --scenario=hidden-wall-diagnostics-plan-light|dark —
оба passed (пиксельное совпадение с текущим эталоном, не только «семантика»).
Для полной картины поднял фактический прогон Validate на этой же ветке этим же автором
(GitHub Actions, gh run view/api):
- commit
a2b4d32b(сама правка, до докс-коммита), run32854408646: jobgolden→ failure. Полный список сцен смотрел через логи джобы; 110 сценpassed, 5 сценdifferent:safe-resize-handles-clamp-light,safe-resize-handles-clamp-dark,opening-placement-door-thick-wall-dark,opening-placement-passage-thick-wall-dark,opening-placement-passage-thick-wall-light. Смокsmoke (1)иperformance_smoke—cancelled(не успели: следующий пуш отменил прогон),docs—failure(ожидаемо, устаревший отпечаток скриншотов, закрыто следующим коммитом). - те же 5 сцен на
dev(run32848267582, jobgolden, commit2143e888) — все passed. Разница вызвана этой веткой, не флуктуацией CI. - скачал артефакт
golden-images(gh api .../artifacts/9565735845/zip) и посмотрелdiff/safe-resize-handles-clamp-light.png,diff/opening-placement-door-thick-wall-dark.png,diff/opening-placement-passage-thick-wall-light.png— во всех трёх новый статический слой осей/узлов (тот самыйplan-snap-line/plan-snap-node) закономерно появляется поверх сцены, потому чтоprepareGoldenScenarioдля этих сценариев (safeResizePreview→card._tool = 'resize',openingPreview→card._activateOpeningPlacement(type)переводит в_tool = 'opening') не возвращает_toolвdrawперед снимком — в отличие от нового кода дляscenario.hiddenWallDiagnostics, который явно делаетcard._tool = 'draw'перед финальным кадром (demo/golden/harness.mjs:709-711). См. finding ниже.
Разбор по AC
- AC1 (паритет контрольного узла, smoke+golden). Смок: новая fixture «room + 2 coincident
partitions» даёт 6/6 узлов и линий в
drawиwallthick(thicknessShowsCompleteSixNodeFixture). Golden: существующие сценыhidden-wall-diagnostics-plan-{light,dark}(комната + coincident partition + скрытая сохранённая цепочка) проходят пиксельно без изменений эталона, и новая проверка внутриprepareGoldenScenario(строки 675-711) явно перебирает все 10 инструментов и бросает исключение при расхождении DOM до снятия скриншота. Выполнено. - AC2 (все инструменты, smoke).
allPlanToolsShareStaticAxesAndNodesсравнивает нормализованный DOM-снимок (data-key, координаты) по всем 10MarkupTool;allPlanToolOverlaysPaintAboveWallBodiesподтверждает порядок относительно.wallbodies. Выполнено. - AC3 (дедупликация, unit+smoke). Новый unit-тест
test/plan-snap-overlay.test.mjsфиксирует 6 сегментов/6 узлов для той же топологии на уровнеbuildPlanSnapGeometry(сама функция не менялась — дифsrc/plan-snap-overlay.tsпуст, что и ожидалось: контракт дедупликации не задет). Утверждение «hidden diagnostic не создаёт визуального двойника» — унаследованное поведение нетронутойbuildHiddenWallDiagnosticGeometry(уже сосуществовала с полным overlay вdrawдо фикса); проверено чтением, не новым тестом. Выполнено. - AC4 (интерактивность, smoke).
allPlanToolOverlaysStayPointerTransparentпроверяетpointer-events: noneи оба вычисляемых стиля во всех 10 инструментах. Вторая часть AC4 («применение толщины меняет только выбранный интервал») не тестировалась заново на новой fixture — код применения толщины (_wallDialogи связанный путь) диффом не тронут, оверлей былpointer-events: noneи до фикса; проверено чтением.smoke_wall_thickness.mjs(несвязанная fixture) остаётся зелёным. Выполнено, вторая половина — «проверено чтением, не исполнением». - AC5 (границы режима, smoke).
nonPlanModesHaveNoOverlayпроверяетdevices,decor,viewбез слоя;_markup(не изменён) гарантирует то же на уровне кода. Отсутствие промежуточного кадра без осей при переключении инструмента (контракт п.7) — структурно невозможно: видимость зависит только от_markup, не от_tool;_toolне участвует в тайминге рендера. Проверено чтением. Выполнено. - AC6 (доступность и регрессии, smoke+golden).
forcedColorsStayReadableзелёный (существующая проверка, поведение не изменено). Целевые смоки draw/openings/resize (см. список выше) зелёные. Но golden-доказательство «без изменения поведения» для двух смежных сценариев неполно — см. finding H1. Частично выполнено. - AC7 (данные и производительность, unit + ревью кода). Диф не трогает
_planSnapGeometrySnapshot, кеш или что-либо на pointermove; конфигурация не мутируется, сохранение не вызывается при смене инструмента (не добавлено ни одного нового вызова записи). Проверено чтением: единственное изменение — булево условие рендера. Выполнено.
Находки
H1 (High) — два golden-сценария неродственных фич стали «different» на канонической Linux CI и не получили новый эталон
Файлы: demo/golden/harness.mjs (safeResizePreview, openingPreview — строки 946-1033),
demo/golden/matrix.mjs (сценарии safe-resize-handles-clamp-{light,dark},
opening-placement-{door,passage}-thick-wall-{dark,light}), demo/golden/baselines/*.png (не
обновлены).
Воспроизведение: GitHub Actions, репозиторий Matysh/houseplan-card, run 32854408646
(Validate на commit a2b4d32b, та же ветка), job golden →
https://github.com/Matysh/houseplan-card/actions/runs/32854408646/job/97823146408 — статус
failure; 5 из 115 сцен: different. Те же 5 сцен на dev (run 32848267582, job golden,
commit 2143e888) — все passed, то есть расхождение вызвано именно этой веткой, не шумом CI.
Скачанные diff-изображения (golden-images artefact, 9565735845) показывают новый слой
plan-snap-line/plan-snap-node, появившийся в сценах resize и opening, где раньше рендерился
пустой <g>.
Причина: контракт issue (п.1) прямо требует показывать статический слой во всех 10
инструментах, включая resize и opening — то есть новое появление осей/узлов в этих golden-сценах
корректно и ожидаемо. Но prepareGoldenScenario для scenario.hiddenWallDiagnostics явно
возвращает card._tool = 'draw' перед финальным кадром (demo/golden/harness.mjs:709-711),
а scenario.safeResizePreview (переводит в resize) и scenario.openingPreview (переводит в
opening) — не возвращают. Пять существующих, не относящихся к #304 сцен (safe-resize-handles-clamp-*,
opening-placement-*-thick-wall-*) снимаются в этих инструментах и теперь легитимно отличаются
от принятого эталона, но новый эталон не снят и не принят: git diff origin/dev...HEAD -- demo/golden/baselines/ пуст.
Почему это блокирует, а не Low: AC6 требует «без изменения поведения» для этих же
инструментов, а issue прямо перечисляет visual/golden доказательство как обязательный
release-артефакт (раздел «Release-артефакты»). Сейчас доказательство — отрицательное: канонический
Linux-гейт красный для пяти сцен, и это не отражено ни в одном коммите, ни в хендофф-комментарии
автора («golden:verify на native Windows — diagnostic only… Канонический полный Linux golden
остаётся обязательным pre-beta gate»), хотя автор сам инициировал этот прогон и мог его увидеть.
Без действия эти пять эталонов останутся неразрешённо «different» на предрелizном гейте — дефект,
который дешевле поймать сейчас (issue #237 — тот же класс пропуска, только для check-docs).
Соответствует условию жёлтого вердикта из процесса: «изменение… ухудшает смежный [сценарий]»
(§7.2/AGENTS.md) — с точки зрения непринятого эталона резинка Resize и предпросмотр проёма в
толстой стене визуально изменились без прошедшего ревью.
Что нужно для исправления (в скоупе этой же задачи, не отдельный issue — причина в диффе этой ветки):
- на Linux (CI, не native Windows) снять
npm run build && npm run bundle:sync && npm run golden:captureдля матрицы; проверить, что «different» ограничивается именно этими 5 сценами (или объяснить, если появились новые); - принять новые эталоны
npm run golden:accept -- --reviewed --from=<распакованный артефакт Linux CI>— важно: текущийValidateна самом HEAD (7310ce0, run32855061198) сам пропустил jobgolden(changesклассифицирует диф относительно предыдущего пуша, а неorigin/dev, и коммит7310ce0— только докс/скриншоты); чтобы получить свежий полный Linux-артефакт для приёмки, нужен новый пуш/ре-триггерValidateна этой ветке; - отразить пересъёмку в хендофф-комментарии (команда + результат, не «verified»).
Low (снято без правки, не блокирует)
- AC1 фиксирует «шесть узлов» через частный unit/smoke fixture, а не через golden-сцену с
буквально такой же топологией (golden использует смежный, но не идентичный
golden-coincident-partitionфикстур). Разумно — тот же класс регресса (совмещённые/скрытые оси) реально покрыт, дублировать фикстуру в третьем месте не нужно. - Комментарий-документация в шаблоне (
${''/* … */}) — существующий в файле стиль (см. строки 17560, 17568), новый блок ему соответствует, замечаний нет.
Что проверено и корректно
- Гейтинг сведён к одному булеву условию, transient state (
_activePlanSnapCandidate,_activePlanSnapConflicts) остаётся эксклюзивным «Стенам» — контракт п.4 не нарушен. - Порядок слоёв не менялся (условие снято на уже существующем месте в шаблоне) — риск «render order перекрыл preview другого инструмента», названный в issue, не реализовался нигде, кроме golden-сцен из H1, где это ожидаемо и просто не задокументировано эталоном.
- View/Device editor/Background editor не получают слой ни по коду, ни по смоку.
- i18n не тронут (нет диффа в
src/i18n/*,custom_components/**/translations/*), миграции и compatibility-поля не нужны — конфигурация иplan-snap-overlay.tsне менялись. - Трейлеры: оба коммита несут
Issue: #304;a2b4d32b—User-Visible: yesс правками в обоих changelog в том же коммите;7310ce01—User-Visible: no, докс-только, скриншоты снял workflowDocs screenshots(run32854424829, конклюзияsuccess, SHA совпадает сa2b4d32b) — процесс скриншотов (§8 «только джобой Docs screenshots») соблюдён. node scripts/process-gate.mjs --range=origin/dev..HEAD— «гейт пройден», единственноеWARN(нетdocs/specs/304-*.md) — ожидаемо дляsmall-трека.- Один источник числа: диф не вводит новую видимую пользователю величину (только топологию линий/узлов), правило «одно число — один источник» неприменимо.
Чего не проверял
- Полный
demo/smoke_*.mjs(188 файлов) и полныйnpm run golden:verifyлокально — не запускал, задача не задевает все поверхности; вместо этого использовал целевую выборку (smoke-select.mjs) плюс фактический канонический Linux-прогон автора для golden (сильнее локального прогона на native Windows). python -m pytest tests_backend— не запускал,custom_components/**/*.pyне тронут.- Performance-профили и large-house benchmark — не запускал:
_planSnapGeometrySnapshot/резолвер не тронуты, влияние на pointermove отсутствует по чтению кода, в AC не названо. node scripts/model-invariants.mjs— не запускал: диф не касается рёбер комнат,layout,marker.space,open_spans, записей толщины.- Визуальную читаемость двух новых/скорректированных строк
docs/USER-GUIDE.md/.ru.mdсмотрел только текстом; PNG-скриншоты (docs/images/*.png) не сверял пиксельно — доверился принятому workflowDocs screenshotsиcheck-docs.mjs.
Итог
Продуктовая правка минимальна, соответствует контракту, транзитное состояние и границы режимов не
нарушены — подтверждено и чтением, и смоками, включая доказанную способность нового смока и нового
golden-инварианта падать. Но обязательное по контракту и по разделу «Release-артефакты» golden-
доказательство неполно: два неродственных, уже существующих сценария (safe-resize-handles-clamp-*,
opening-placement-*-thick-wall-*) корректно и ожидаемо изменили внешний вид под тем же самым
кодом, но не получили пересъёмку/приёмку эталона, и это осталось незамеченным/незадокументированным
в хендоффе при том, что канонический Linux CI уже показал golden: failure на этой ветке.
Возврат автору для пересъёмки и приёмки этих пяти эталонов на Linux — фикс укладывается в эту же
задачу (причина в её собственном диффе), отдельный issue не нужен.