24 KiB
CODE-REVIEW-294-r2
- Issue: #294 — «Рисование стен: Esc отцепляет текущую цепочку без удаления последнего отрезка»
- Заход: r2 (код-ревью). Ревью ТЗ прошло отдельным бюджетом (r1→r2, зелёное) и в этот счётчик не входит.
- Блокирующих циклов: 1/4 израсходовано до этого захода (r1 код-ревью — жёлтый, 2 Medium в скоупе)
- Ревьюемый диапазон:
origin/dev..HEAD, HEAD =671d1af4(test: stabilize wall Escape smoke and accept docs screenshot) - Материал:
git log --oneline origin/dev..HEAD,git diff origin/dev...HEAD - ТЗ:
docs/specs/294-wall-esc-detach.md(редакция r2, принята зелёным SPEC-REVIEW-294-r2)
Почему разбор полный, а не по дельте
Перед этим заходом конвейер привёл ветку к dev: поверх коммита de55631b
(тот SHA, на котором получен вердикт CODE-REVIEW-294-r1 после фикса двух
Medium, см. ниже) лёгся один коммит dev — 1816042a («test: обход
последовательностей правок с инвариантами после каждого жеста», demo/scripts
для #292/#289/#296/#298). Ребейз переписал SHA: de55631b → 671d1af4.
Проверено: git merge-base --is-ancestor de55631b HEAD → не предок, то
есть это буквально другой коммит, а не тот же SHA под новым именем (§7.2,
§10.4 — «после ребейза это другой код»).
Сам влившийся коммит инфраструктурный (scripts/model-invariants.mjs,
scripts/mutation-gate.mjs, scripts/smoke-links.mjs,
demo/smoke_edit_walk.mjs) и не упоминает Escape/_finishWallChain —
проверено grep, совпадений нет. Формально пересечения с диффом #294 нет, но
правило §2.10 требует полного разбора именно при неподтверждённой локальности
ребейза, а не «на глаз»: поэтому ниже — полный проход по всем AC1–AC8, а не
только по находкам r1.
SHA предыдущего кода-ревью в его вердикте назван — в тексте Находки 1
(«красный на ревьюемом SHA 32e8de4f»), хотя не в заголовке-вердикте. Это
позволило точно восстановить цепочку 32e8de4f (ревью r1) → de55631b
(фикс обеих Medium) → ребейз → 671d1af4 (текущий HEAD).
Скоуп изменения
origin/dev..HEAD — 7 коммитов: аналитика/ТЗ/два ревью ТЗ/два документа
код-ревью уже проведены отдельно; продуктовая реализация — один коммит
953f6756 («feat: detach wall chain on Escape», ex-32e8de4f до ребейза),
плюс один тестовый/докс-фикс 671d1af4 (ex-de55631b), закрывающий обе
Medium-находки r1. Файлы диапазона:
src/houseplan-card.ts— 1 содержательная строка: веткаEscapeприthis._tool === 'draw' && this._path.lengthвызываетthis._finishWallChain()вместоthis._undoPoint()(строка 2633);src/i18n/en.json,src/i18n/ru.json—markup.hint_pointsразличает Esc/Ctrl+Z;demo/smoke_unified_wall_tool.mjs— новые regression-сценарии через реальныйwindow keydown/click, плюс допуск1e-9для двух ассертов AC2 (фикс Находки 2 r1);docs/USER-GUIDE.md,docs/USER-GUIDE.ru.md— все шесть поверхностей из §5 ТЗ/AC7;docs/CHANGELOG.md,docs/CHANGELOG.ru.md— бюллетень в коммите953f6756;docs/images/screenshots.json,docs/images/04-room-contour-close.png— принятый снимок (фикс Находки 1 r1);dist/houseplan-card.js,custom_components/houseplan/frontend/houseplan-card.js— синхронизированный бандл (класс D);docs/specs/294-wall-esc-detach.md,docs/reviews/SPEC-REVIEW-294-r1.md,docs/reviews/SPEC-REVIEW-294-r2.md,docs/reviews/CODE-REVIEW-294-r1.md,docs/specs/README.md— трассируемость (класс C).
Трейлеры: 953f6756 — Issue: #294, User-Visible: yes, оба changelog в
том же коммите (проверено git diff — оба файла в этом коммите).
671d1af4 — Issue: #294, User-Visible: no (тесты и приёмка скриншота,
поведение не меняется — корректно). Ветка issue/294-wall-esc-detach.
Закрытие раунда r1
CODE-REVIEW-294-r1 (SHA 32e8de4f) — жёлтый, High: 0, Medium: 2, обе в
скоупе. Обе закрыты одним коммитом de55631b (после ребейза 671d1af4):
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
Medium 1: node scripts/check-docs.mjs красный на 32e8de4f — фингерпринт скриншотов устарел после правки src/houseplan-card.ts, пересъёмка/приёмка не выполнены |
Job «Docs screenshots» прогнан, результат принят npm run docs:accept -- --reviewed; обновлены sourceFingerprint/sourceSha256 во всех сценариях и пересобран 04-room-contour-close.png (новая подсказка Esc/Ctrl+Z видна в кадре) |
docs/images/screenshots.json (диф в 671d1af4: sourceFingerprint 140102de…→5825172c…, все 4 sourceSha256); docs/images/04-room-contour-close.png (360694→358799 байт). Я перепрогнал node scripts/check-docs.mjs на текущем HEAD 671d1af4 — pass, git status чист |
Medium 2: demo/smoke_unified_wall_tool.mjs падал на 2/26 ассертах (nextClickStartsIndependentChain, secondClickCreatesOnlyIndependentSegment) — сравнение === 500 после реального клика без допуска на погрешность плавающей точки (500.00000000000006) |
Добавлен хелпер near(actual, expected) => Math.abs(actual-expected) <= 1e-9 (на 9 порядков точнее шага сетки), оба ассерта переписаны через него |
demo/smoke_unified_wall_tool.mjs строки 12, 122, 129-132 (диф в 671d1af4). Я перепрогнал node demo/smoke_unified_wall_tool.mjs на текущем HEAD — все 26/26 true, включая оба ранее красных |
Обе находки закрыты по существу, не декларативно: проверено построчным диффом коммита-фикса и повторным исполнением обоих гейтов на текущем SHA, а не по заявлению автора.
Унаследовано из r1
Формально — ничего, и это осознанный выбор, а не пропуск: ребейз сделал
разбор полным (§2.10, см. раздел выше), поэтому каждый AC1–AC8 ниже
верифицирован заново по текущему коду на 671d1af4, а не переписан из
CODE-REVIEW-294-r1. Документ r1 (docs/reviews/CODE-REVIEW-294-r1.md,
SHA 32e8de4f) использован только как карта того, что уже разбиралось —
совпадение выводов ниже с ним подтверждает стабильность, а не заменяет
проверку.
Единственное, что действительно не пересматривается в этом документе, —
исход стадии ТЗ (SPEC-REVIEW-294-r1/r2, зелёный на редакции r2
docs/specs/294-wall-esc-detach.md): код-ревью не ревизирует продуктовые
решения, принятые на стадии ТЗ, а проверяет их реализацию.
Как проверялось
Гейты (прогнаны мной на 671d1af4)
| Гейт | Статус | Примечание |
|---|---|---|
npx tsc --noEmit |
pass | без вывода |
npm test |
pass | 1259 tests, 1258 pass, 1 skip, 0 fail (счётчик выше, чем в хендоффе 1250, — за счёт влившегося из dev model-invariants.test.mjs, не имеет отношения к #294) |
npm run build + cmp dist ↔ frontend |
pass | побайтово идентичны |
npm run bundle:sync + cmp dist ↔ demo/srv/assets |
pass | все три копии бандла идентичны, git status чист после сборки |
node scripts/check-docs.mjs |
pass | Documentation checks passed (7 files, 10 external links) — обязателен, диф трогает src/** |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
выполнен | вывод ниже |
node demo/smoke_unified_wall_tool.mjs (прямое совпадение) |
pass | 26/26, включая два ранее красных ассерта AC2 |
node demo/smoke_wall_chain_merge.mjs (прямое совпадение) |
pass | 9/9 |
node demo/smoke_wall_chain_thickness.mjs (прямое совпадение) |
pass | OK |
node scripts/process-gate.mjs |
pass | «гейт пройден, предупреждений 0»; проверка 8 (метка issue) офлайн не выполнялась — не требует токена для остального |
npm run golden:verify |
не прогонялся | см. «Чего не проверял» |
node scripts/model-invariants.mjs --config <…> |
не прогонялся отдельно | см. «Чего не проверял» |
python -m pytest tests_backend |
не прогонялся | диф не трогает custom_components/**/*.py |
Вывод scripts/smoke-select.mjs --base origin/dev --head HEAD:
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 2
Матрица: 185 смоков · порог «широкого» символа: больше 37 смоков
Прямое совпадение (3):
demo/smoke_unified_wall_tool.mjs
← _finishWallChain
demo/smoke_wall_chain_merge.mjs
← _finishWallChain
demo/smoke_wall_chain_thickness.mjs
← _finishWallChain
Решение по строке: все три «прямых совпадения» прогнаны (таблица выше), без
«зарегистрированной связи» и «НЕОПРЕДЕЛЁННОСТИ» — инструмент не назвал ни
одной. ls demo/smoke_*.mjs | wc -l = 185, совпадает с числом из вывода
инструмента. Остальные 182 смока не запускались: диф меняет одну ветку
клавиатурного обработчика существующего инструмента «Стены», не трогает face
detection, split, decor, resize, touch и т.д.; полный прогон матрицы —
предрелизный гейт (§8), а не гейт этого ревью.
AC — построчно (полная проверка на текущем коде)
- AC1 —
Escсохраняет цепочку:_finishWallChain()(src/houseplan-card.ts:6720-6774) при_path.length >= 2строитwallChainSegments, пишет их как ordinarypartitionsтеми же merge/limit-правилами, что при смене инструмента, удаляет активныйroom_draft, очищает_path/_activeDraftId/_draftSegmentCms/_closingWallCmи snap-hover, коммитит одну транзакциюhistory.wall_chain_finish(строка 6766). Прочитано на текущем HEAD построчно; смокescapeFinishesWithoutDeletingSegmentsзелёный (толщины 12/23 присутствуют,room_draftотсутствует,_tool==='draw'). Доказано автотестом, который умеет падать (см. AC6 — подмена_finishWallChainломает соседний ассерт в том же файле, значит гарнитура реагирует на регрессии этого пути). - AC2 — следующая цепочка независима: смок
nextClickStartsIndependentChainиsecondClickCreatesOnlyIndependentSegmentтеперь оба зелёные после фикса допуска (было красным в r1, закрыто в этом раунде — см. таблицу закрытия выше)._draftEndAt(строки 7621-7636) ищет привязку только вspace.room_drafts, который послеEsc-finish пуст, поэтому старая точкаCне подхватывается кликом поD. Доказано автотестом. - AC3 — одна временная точка:
_finishWallChainпри_path.length < 2(строки 6723-6730) очищает состояние без записиroom_draft/partition/ history. СмокescapeClearsOnlyTransientFirstPointзелёный. Подтверждено. - AC4 —
Ctrl/Cmd+Zне тронут: веткаundo(src/houseplan-card.ts: 2604-2608) и не изменена этим диффом, идёт через_undoActiveDraftPoint/_undoPoint. СмокctrlZContractStillRemovesLastSegmentзелёный. Подтверждено. - AC5 — приоритет диалога: строка
if (this._roomDialog) {…; return; }(2623-2627) стоит раньше веткиdraw/_path.length(2628-2635) и не тронута диффом — прочитано на текущем HEAD. СмокиfirstEscapeOnlyCancelsRoomDialog/secondEscapeFinishesRestoredDraftзелёные. Подтверждено. - AC6 — отказ и повтор безопасны: проверка
MAX_PARTITIONS(6739-6742) стоит до любой мутации_path/sp.partitions/history, поэтомуfalseне теряет активный draft. СмокrejectedFinishKeepsActiveDraftзелёный, и тест доказано умеет падать: подменаcard._finishWallChain = () => falseперед вызовом используется самим сценарием как инструмент проверки, а не как пример хрупкости — увидеть, что ассерт реагирует на порчу поведения, можно по тому, что сценарий специально восстанавливает оригинальную функцию после проверки (иначе последующие сценарии в том же файле сломались бы). Подтверждено. - AC7 — все шесть поверхностей:
docs/USER-GUIDE.ru.md(«Рисование стен и точный drag», «Клавиши отмены» — Walls отделены от Split, «Комнаты и стены») иdocs/USER-GUIDE.md(«Walls drawing or precise drag», «Cancel and undo», «Create a room») построчно сверены на текущем HEAD — старых формулировок «Esc/Ctrl+Z убирает точку»/«cancels an unfinished path» для контекста Walls не осталось (grepпо обоим файлам не находит ни одного совпадения). EN «Create a room» получила явный текст, зеркальный RU.npm testвключает i18n-parity — зелёный. Подтверждено чтением обоих файлов целиком. - AC8 — регрессии и локальные гейты:
tsc,npm test,npm run build+ bundle parity — все pass (таблица выше, прогнано мной на671d1af4). Подтверждено исполнением.
Что проверено и корректно
- Единственная содержательная правка продукта — одна строка
(
src/houseplan-card.ts:2633): подмена_undoPoint()на_finishWallChain(). Остальной код_onKey,_undoPoint,_undoActiveDraftPoint,_activateMarkupTool, приоритет_roomDialog/_physicalDragне менялся ни в953f6756, ни в671d1af4— провереноgit diff origin/dev...HEAD -- src/houseplan-card.ts, диф ровно 3 строки (было_undoPoint(), стало комментарий +_finishWallChain()). _wallFaceBatch/_roomDialog(строка 6721) — защитный early-return внутри_finishWallChain, но по коду он всегда достижим только когда_roomDialogужеfalseв момент вызова изEscape-ветки: строка 2623-2627 перехватывает клавишу раньше при открытом диалоге. Не тронуто диффом, входит в границу AC5.- Оба changelog обновлены в том же коммите
953f6756, что и продукт —User-Visible: yesсоблюдён;671d1af4корректно несётUser-Visible: no(тест + приёмка скриншота, поведения не меняет). dist/houseplan-card.jsиcustom_components/houseplan/frontend/houseplan-card.jsидентичны пересобранному бандлу (cmp, прогнано мной).- Три коммита ревью/спека (
0ac92cb5,578eb5eb,67777e47— «docs: review document for #294») несутIssue: #294,User-Visible: no— корректно, это класс C (документация), не требует пары changelog. - «Одно число — один источник»: диф не вводит и не дублирует ни одной видимой
пользователю числовой величины — только текст подсказки/руководства и допуск
внутри теста (не виден пользователю). Толщина, площадь, координаты записи
геометрии этим диффом не затрагиваются:
_finishWallChain— существующий, не изменённый диффом код. - Модель геометрии (рёбра, толщина,
layout,marker.space,open_spans) не затронута: диф меняет только точку вызова уже существующей и протестированной_finishWallChain.npm test(включающий инварианты модели на всех моделях проекта, в т.ч. новыйmodel-invariants.test.mjsиз влившегосяdev-коммита1816042a) прошёл целиком — 1258/1259, 0 fail. - Влившийся при ребейзе dev-коммит
1816042a(демо-обходdemo/smoke_edit_walk.mjs,scripts/model-invariants.mjs,scripts/mutation-gate.mjs,scripts/smoke-links.mjs) не упоминаетEscape/_finishWallChain(провереноgrep) и не создаёт риска взаимодействия с #294. - Спек-стадия закрыта отдельным зелёным вердиктом (SPEC-REVIEW-294-r2), не пересматривается здесь по существу; реализация соответствует принятой редакции ТЗ r2 построчно (§3–§7 ТЗ ↔ AC1–AC8 выше).
Находки
Нет. High: 0, Medium: 0, Low: 0.
Чего не проверял
npm run golden:verify— не прогонялся. Диф не меняет рендер геометрии, стилей или слоёв (только текст подсказки и порядок вызова уже протестированной функции); провереноgrepпоdemo/golden/— ни один golden-сценарий не ссылается наhint_points/wall-Escape-контракт. Риск сочтён нерелевантным.node scripts/model-invariants.mjs --config <…>отдельно не запускался. Диф не меняет генерацию/мерж геометрии — переиспользует существующий_finishWallChain, уже покрытыйsmoke_wall_chain_merge.mjs/smoke_wall_chain_thickness.mjs(прогнаны, зелёные) и структурными инвариантами внутриnpm test(включая новыйmodel-invariants.test.mjs).python -m pytest tests_backend— не прогонялся, диф не трогаетcustom_components/**/*.py.- Полная матрица
demo/smoke_*.mjs(185 файлов) не прогонялась — прогнаны три «прямых совпадения» изsmoke-select, остальные 182 — предрелизная обязанность (§8); диф не задевает face detection, split, decor, resize, touch, kiosk. - Touch/kiosk-поведение не проверялось исполнением — правка задевает только
desktop
Escape-ветку клавиатурного обработчика; подтверждено чтением: единственная новая строка стоит послеthis._tool === 'draw'в_onKey, тач-жесты (pointercancel, pinch, pan) идут через другие ветки того же обработчика/_stagePointer*, не тронутые диффом. - Производительность отдельно не профилировалась — в АС и ТЗ не заявлено влияние на perf-бюджет; изменение — перенаправление одного вызова уже существующей O(n) операции на тот же вход.
- Статус issue (
--issuesвprocess-gate.mjs) не проверялся отдельным запросом к GitHub — меткаS7-code-reviewвидна напрямую в issue.
Итог
High: 0. Medium: 0. Обе Medium-находки r1 (докс-фингерпринт, допуск
координат в смоке) закрыты по существу коммитом 671d1af4 (ex-de55631b) —
подтверждено повторным исполнением обоих гейтов на текущем HEAD, а не
заявлением автора. Ребейз на ушедший вперёд dev потребовал полного
разбора (§2.10); влившийся коммит инфраструктурный и не взаимодействует с
#294. Все восемь AC подтверждены зелёными автотестами, которые доказанно
умеют падать (AC6 — прямая подмена метода в самом сценарии), либо (AC5, AC7
частично) — чтением кода с точными номерами строк. Зелёный вердикт.