Files
houseplan-card/docs/reviews/CODE-REVIEW-294-r2.md
T
2026-08-24 18:25:12 +00:00

274 lines
24 KiB
Markdown

# 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`, пишет их как ordinary `partitions` теми же
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
частично) — чтением кода с точными номерами строк. Зелёный вердикт.