20 KiB
CODE-REVIEW-294-r1
- Issue: #294 — «Рисование стен: Esc отцепляет текущую цепочку без удаления последнего отрезка»
- Заход: r1 (первый код-ревью-цикл; ревью ТЗ прошло r1→r2 отдельно и не считается в этом бюджете)
- Блокирующих циклов: 0/4 израсходовано до этого захода
- Ревьюемый коммит:
32e8de4f(«feat: detach wall chain on Escape»), веткаissue/294-wall-esc-detach - Материал:
git log --oneline origin/dev..HEAD,git diff origin/dev...HEAD - ТЗ:
docs/specs/294-wall-esc-detach.md(редакция r2, принята зелёным SPEC-REVIEW-294-r2)
Скоуп изменения
Диапазон origin/dev..HEAD — пять коммитов: аналитика/ТЗ/два ревью ТЗ уже
приняты отдельно, реализация — один коммит 32e8de4f. Файлы этого коммита:
src/houseplan-card.ts— 1 содержательная строка:Escapeв веткеthis._tool === 'draw' && this._path.lengthтеперь вызываетthis._finishWallChain()вместоthis._undoPoint();src/i18n/en.json,src/i18n/ru.json—markup.hint_pointsразличает Esc/Ctrl+Z;demo/smoke_unified_wall_tool.mjs— новые regression-сценарии через реальныйwindow keydownиstageclick;docs/USER-GUIDE.md,docs/USER-GUIDE.ru.md— все шесть поверхностей, перечисленных в §5 ТЗ и требуемых AC7;docs/CHANGELOG.md,docs/CHANGELOG.ru.md— бюллетень в том же коммите;dist/houseplan-card.js,custom_components/houseplan/frontend/houseplan-card.js— синхронизированный бандл (класс D, сгенерированоbundle:sync).
Трейлеры коммита: Issue: #294, User-Visible: yes — оба changelog в этом же
коммите. Корректно.
Как проверялось
Гейты
| Гейт | Статус | Примечание |
|---|---|---|
npx tsc --noEmit |
pass | без вывода |
npm test |
pass | 1251 tests, 1250 pass, 1 skip, 0 fail — совпадает с хендоффом |
npm run build + cmp dist ↔ frontend |
pass | сборка идентична закоммиченным dist/ и custom_components/.../frontend/ |
npm run bundle:sync (для стенда) |
выполнен локально ревьюером | demo/srv/assets/houseplan-card.js не коммитится (#255), пересобран для прогона смоков |
node scripts/check-docs.mjs |
FAIL | см. Находку 1 — обязателен, диф трогает src/** |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
выполнен | вывод ниже |
node demo/smoke_unified_wall_tool.mjs (прямое совпадение) |
FAIL (2 из 26) | см. Находку 2 |
node demo/smoke_wall_chain_merge.mjs (прямое совпадение) |
pass | 9/9, без регрессий |
node demo/smoke_wall_chain_thickness.mjs (прямое совпадение) |
pass | OK |
npm run golden:verify |
не прогонялся | см. «Чего не проверял» |
npm run invariants / geometry model |
не прогонялся отдельно | см. «Чего не проверял» |
python -m pytest tests_backend |
не прогонялся | диф не трогает custom_components/**/*.py |
Вывод scripts/smoke-select.mjs --base origin/dev --head HEAD:
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 2
Матрица: 184 смоков · порог «широкого» символа: больше 36 смоков
Прямое совпадение (3):
demo/smoke_unified_wall_tool.mjs
← _finishWallChain
demo/smoke_wall_chain_merge.mjs
← _finishWallChain
demo/smoke_wall_chain_thickness.mjs
← _finishWallChain
Решение по строке: все три «прямых совпадения» прогнаны (таблица выше). Остальные 181 смоков не запускались — задача меняет одну ветку клавиатурного обработчика существующего единого инструмента «Стены», не трогает face detection, split, decor, resize, touch и т.д.; полный прогон матрицы — гейт предрелиза (§8), не этого ревью.
AC — построчно
- AC1 (
smoke): доказан кодом и частично смоком._finishWallChain()приthis._path.length >= 2конвертирует сегменты вpartitionsтой же функцией, что и при смене инструмента, удаляетroom_draft, очищает_path/_activeDraftId/_draftSegmentCms/_closingWallCmи снэп-hover (src/houseplan-card.ts:6720-6774). АссертescapeFinishesWithoutDeletingSegmentsв смоке зелёный. **Проверено чтением- прогоном смока.**
- AC2 (
smoke): код проверен чтением и подтверждён отдельным отладочным прогоном, но штатный смок красный — см. Находку 2. Причина красного — дефект теста (сравнение чисел с плавающей точкой), не продукта: код разбора клика падает в ту же_markupClick→_path.length === 0ветку → новая точка становится первым узлом чистого пути, а_draftEndAtищет только вspace.room_drafts, который послеEsc-finish пуст, поэтому старая точкаCне подхватывается (src/houseplan-card.ts:7565-7591,7621-7636). AC2 подтверждён чтением кода и точечной проверкой (см. Находку 2), но не зелёным автотестом — до фикса теста это открытый пункт. - AC3 (
smoke):escapeClearsOnlyTransientFirstPointзелёный; веткаthis._path.length < 2в_finishWallChainочищает состояние без записиroom_draft/partition/history (6723-6730). Подтверждено. - AC4 (
smoke):ctrlZContractStillRemovesLastSegmentзелёный;Ctrl/Cmd+Zне тронут этим диффом и по-прежнему идёт через_undoActiveDraftPoint/_undoPoint(2588-2608). Подтверждено. - AC5 (
smoke):firstEscapeOnlyCancelsRoomDialogиsecondEscapeFinishesRestoredDraftзелёные; в_onKeyпроверкаif (this._roomDialog) { …; return; }(2623-2627) стоит раньше веткиdraw/_path.length(2628-2635) и не тронута диффом. Подтверждено. - AC6 (
smoke+ чтение):rejectedFinishKeepsActiveDraftзелёный; проверкаMAX_PARTITIONSв_finishWallChain(6739-6742) выполняется раньше любой мутации_path/sp.partitions/history, поэтомуfalseне теряет состояние. Подтверждено. - AC7 (
unit+ ревью кода): все шесть поверхностей из §5 ТЗ построчно сверены сdocs/USER-GUIDE.md/docs/USER-GUIDE.ru.md— см. раздел «Что проверено и корректно».test/i18n-parityчастьnpm testзелёная. Подтверждено чтением, без старых противоречащих формулировок. - AC8 (
unit+build):tsc,npm test,npm run build+bundle parity — все pass (таблица гейтов). Подтверждено исполнением.
Находки
Находка 1 (Medium, в скоупе — правится в этом issue)
Обязательный гейт node scripts/check-docs.mjs красный на ревьюемом SHA.
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
Воспроизведение: на origin/dev (8c036564) тот же скрипт зелёный —
Documentation checks passed (7 files, 10 external links). На 32e8de4f
(этот диф) — красный, потому что docs/images/screenshots.json.sourceFingerprint
считается по всему src/** (#245/#246), а src/houseplan-card.ts изменён, и
пересъёмка/принятие скриншотов в этом коммите не выполнены.
Это ровно тот гейт, для которого правила ревью прямо говорят «выбирать нечего»
и напоминают цену пропуска: в #230 и #234 его пропустили, и dev простоял с
красным job docs до следующей задачи (#237). Оставлять его красным в этом
ревью значило бы повторить #230/#234.
Правка: прогнать job «Docs screenshots» (workflow_dispatch), принять
результат npm run docs:accept -- --reviewed --from=<артефакт> и закоммитить
обновлённый docs/images/screenshots.json (+ при необходимости PNG) вместе с
задачей, либо явно объяснить в хендоффе, почему для этого текстового изменения
пересъёмка не нужна (в текущем виде это не объяснено и гейт просто красный).
Находка 2 (Medium, в скоупе — правится в этом issue)
Новый таргетированный смок demo/smoke_unified_wall_tool.mjs падает на
двух ассертах, которые как раз доказывают AC2.
FAILED (2):
- nextClickStartsIndependentChain: expected true, got false
- secondClickCreatesOnlyIndependentSegment: expected true, got false
Воспроизведение: npm run bundle:sync && node demo/smoke_unified_wall_tool.mjs
(бандл стенда не коммитится — #255 — поэтому синхронизация нужна перед прогоном).
Причина — не регрессия продукта, а сравнение координат с плавающей точкой без
допуска. Новый хелпер clickStage(x, y) (строки 18-28) переводит логические
координаты в clientX/clientY через rect/view, диспатчит настоящий DOM
click на .stage, а тест затем требует точного равенства
card._path[0][0] === 500 && card._path[0][1] === 500. Отдельным отладочным
прогоном подтверждено: тот же клик (500, 500) на чистой карте даёт
card._path === [[500, 500.00000000000006]] — снэп к сетке в
_resolvePlanDrawPoint/_svgPoint вносит обычную для плавающей арифметики
погрешность 6·10⁻¹⁴. Второй ассерт (secondClickCreatesOnlyIndependentSegment)
делит то же число на NORM_W и сравнивает с 0.5 — получает
0.5000000000000001 и тоже падает. Сам продукт при этом работает верно:
_draftEndAt (строки 7621-7636) ищет привязку только в space.room_drafts,
который после Esc-finish пуст (черновик уже стал partitions), поэтому клик
по D действительно не подхватывает старую точку C — просто тест это не
может подтвердить в текущем виде.
Остальные 8 новых ассертов того же файла (включая три с реальным
window keydown — escapeFinishesWithoutDeletingSegments,
repeatedEscapeAfterFinishIsNoop, rejectedFinishKeepsActiveDraft, а также
диалоговые firstEscapeOnlyCancelsRoomDialog/secondEscapeFinishesRestoredDraft)
зелёные и доказывают AC1, AC3, AC5, AC6 честно — тест умеет падать: замена
card._finishWallChain = () => false перед rejectedFinishKeepsActiveDraft
подтверждает это на месте.
Правка: сравнивать с допуском (как это уже принято для координат в
проекте, например Math.hypot(...) <= eps в _draftEndAt этого же файла)
либо округлять полученную точку перед сравнением, а не требовать === 500
после реального клика через трансформацию viewBox.
Что проверено и корректно
- Единственная содержательная правка продукта — одна строка
(
src/houseplan-card.ts:2630-2633): подмена_undoPoint()на_finishWallChain()в веткеEscapeпри активномdrawи непустом_path. Никакой другой код_onKey,_undoPoint,_undoActiveDraftPoint,_activateMarkupTool, приоритет_roomDialog/_physicalDragне менялся — риск регрессии сосредоточен ровно в этой ветке, и она разобрана по AC1-AC6 выше. _finishWallChainпри отказе (MAX_PARTITIONS) не мутирует_pathдо проверки лимита — active state переживаетfalse(AC6), самостоятельно подтверждено чтением строк 6720-6742 и зелёнымrejectedFinishKeepsActiveDraft.- Приоритет
_roomDialogперед веткойdrawне нарушен: строка2623-2627идёт раньше2628-2635и не тронута диффом (AC5). _resumeDraftBySpaceне сохраняет ссылку на завершённый draft: путьpath.length < 2(без активного_activeDraftId, что гарантировано инвариантом «draft создаётся только приpath.length >= 2», строка 7758) и путь успешного finish (delete this._resumeDraftBySpace[this._space], строка 6767) оба корректны — прочитано и совпадает с ассертами!card._resumeDraftBySpace[card._space].- Все шесть справочных поверхностей из ТЗ §5/AC7 действительно переписаны и
построчно сверены с
docs/USER-GUIDE.md/docs/USER-GUIDE.ru.md; старых формулировок «Esc/Ctrl+Z — убрать точку»/«cancels an unfinished path» для контекста Walls не осталось.markup.hint_pointsвen.json/ru.jsonсовпадает по смыслу с этими описаниями. - Оба changelog обновлены в том же коммите
32e8de4f, что и продукт —User-Visible: yesсоблюдён. dist/houseplan-card.jsиcustom_components/houseplan/frontend/houseplan-card.jsидентичны локально пересобранному бандлу (cmpсовпал).- «Одно число — один источник»: этот диф не вводит и не дублирует ни одной видимой пользователю величины (только текст подсказки/руководства); числовых значений (толщина, площадь) правка не касается.
- Модель геометрии (рёбра, толщина,
layout,marker.space,open_spans) не затронута: изменена только точка вызова уже существующей и протестированной_finishWallChain, а не сама геометрия/merge/лимиты.npm test(включающий инварианты модели на всех моделях проекта) прошёл целиком.
Чего не проверял
npm run golden:verify— не прогонялся. Диф не меняет рендер геометрии, стилей или слоёв (только текст подсказки и порядок вызова уже протестированной функции), и ни один golden-сценарий не ссылается наhint_points/Escape-детач; риск сочтён нерелевантным для этого изменения.node scripts/model-invariants.mjs --config <…>отдельно не запускался. Диф не меняет генерацию/мерж геометрии — переиспользует существующий_finishWallChain, уже покрытыйsmoke_wall_chain_merge.mjs/smoke_wall_chain_thickness.mjs(прогнаны, зелёные) и структурными инвариантами внутриnpm test.python -m pytest tests_backend— не прогонялся, диф не трогаетcustom_components/**/*.py.- Полная матрица
demo/smoke_*.mjs(184 файла) не прогонялась — прогнаны три «прямых совпадения» изsmoke-select; остальные 181 — предрелизная обязанность (§8), задача не задевает face detection, split, decor, resize, touch, kiosk и т.п. - Touch/kiosk-поведение не проверялось руками — правка задевает только
desktop-обработчик
Escape, что подтверждено чтением: единственное добавленное ветвление стоит послеthis._tool === 'draw'в клавиатурном обработчике, тач-жесты (pointercancel, pinch, pan) идут через другие ветки того же_onKey/_stagePointer*, не тронутые диффом. - Производительность отдельно не профилировалась — в АС и ТЗ не заявлено влияние на perf-бюджет, а изменение сводится к перенаправлению одного вызова уже существующей O(n) операции.
Итог
High: 0. Medium: 2, обе в скоупе задачи (обязательный docs-гейт красный; собственный новый смок ломается на двух ассертах из-за сравнения floating-point координат без допуска, хотя доказываемое им поведение AC2 при проверке чтением кода и точечным прогоном подтверждено верным). Без High это жёлтый вердикт, возврат автору на правку в этом же issue — отдельный issue не заводится (обе находки в скоупе, §2.7/#202).