docs: review document for #228

Issue: #228
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-22 10:56:38 +03:00
committed by Sergey Matyunin
parent 691cea074b
commit 55d5a560ad
+194
View File
@@ -0,0 +1,194 @@
# CODE-REVIEW-228-r2
- Issue: [#228](https://github.com/Matysh/houseplan-card/issues/228) — надёжное рисование стен и операции с готовым контуром
- ТЗ: `docs/specs/228-plan-drawing-problems.md` (ревью ТЗ зелёное, r1, #228)
- Ветка: `issue/228-plan-drawing-problems`
- Коммит реализации r1: `2aaef12` (`fix: make plan drawing fail closed`)
- Коммит фикса r2: `172d5d7` (`fix: close plan repair review findings`, `Issue: #228`, `User-Visible: yes`)
- Заход ревью: r2 · блокирующих циклов израсходовано 1 из 4 (r1 был жёлтым и списал единицу; настоящий заход разбирает дельту по §2.10 PROCESS.md / issue #214)
- Ревьюер: Claude (код-ревью), сессия без контекста реализации и без контекста r1-ревью
## 0. Предыдущий раунд и SHA
Вердикт r1 найден в комментарии issue
([issuecomment-5379067039](https://github.com/Matysh/houseplan-card/issues/228#issuecomment-5379067039)):
жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2 (M1, M2, обе в
скоупе). Полный документ — `docs/reviews/CODE-REVIEW-228-r1.md` (в дереве на
этом SHA, коммит `9540627`).
**Замечание к процессу, не к коду:** сам текст вердикта в комментарии issue SHA
не называет — только «Документ: …». SHA восстановлен из шапки документа
(«Коммит реализации: `2aaef12`») и из хендоффа автора чуть выше
(«Реализация: 2aaef12») и хендоффа фикса чуть ниже («предыдущий проверенный
SHA 2aaef12 → фикс 172d5d7»), так что дельта для этого раунда однозначна:
`git diff 2aaef12..172d5d7` (= `git diff 2aaef12..HEAD`, `HEAD` этой ветки —
`172d5d7`). Это не блокирует разбор, но стоит перенести в шаблон вердикта:
именование SHA явно, а не только через ссылку на документ.
Ребейза не было: `git merge-base origin/dev HEAD` не изменился относительно
диапазона задачи, `2aaef12` не достижим из `origin/dev` (задача просто ещё не
слита) — код, проверенный в r1, и код на этом SHA продолжают одну и ту же
историю без переписывания. Дельта локальна, полный разбор не требуется по
§2.10/§7.2 AGENTS.md.
## 1. Скоуп проверки — дельта r1→r2
`git diff --stat 2aaef12..172d5d7`: 10 файлов, +420/−24.
Продуктовый код — только `src/houseplan-card.ts` (+17/−9, две точки:
`_offerExistingWallFace` и `_validateWallRepair`) и новый экспорт в
`src/wall-face-repair.ts` (+15, чистая функция `repairMovesHostedPartition`).
Тесты — `test/wall-face-repair.test.mjs` (+22/−2, новый unit-тест) и
`demo/smoke_plan_drawing_repairs.mjs` (+29, два новых сценария). Остальное —
три синхронные копии бандла, оба changelog (RU+EN, `User-Visible: yes` в том
же коммите) и сам `docs/reviews/CODE-REVIEW-228-r1.md`, добавленный в дерево
отдельным коммитом `9540627` между r1 и фиксом.
Дельта задевает ровно AC6/AC7/AC8 (repair ≤2 см, негативная матрица, room
dialog) и §8.6 ТЗ — те же строки, что и обе находки M1/M2 из r1. Ни одна
другая поверхность (snap-overlay, strict-Shift, delete-room dialog,
room-deletion transaction) не тронута — их AC (AC1–AC5, AC9–AC14) наследуются
из r1 без повторной проверки, раздел 4.
## 2. Как проверялось — гейты
Дешёвые гейты прогнаны полностью (стоят минуты, код изменился):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
| Unit | `npm test` | `# tests 1032 / # pass 1032 / # fail 0 / # skipped 0` (на 1 больше, чем в r1 — ровно новый тест `repairMovesHostedPartition`) |
| Build | `npm run build` | зелёный, `dist/houseplan-card.js` собран за ~12s |
| Синхронность бандла | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `cmp … demo/srv/assets/houseplan-card.js` | обе копии побайтно идентичны, `git status` после сборки чист |
| Named smoke (дельта) | `node demo/smoke_plan_drawing_repairs.mjs` | 16/16 `true`, `OK` |
Дополнительно, специально для дисциплины «тест умеет падать» (§8 PROCESS.md,
это условие честности сокращения гейтов на дельте): временно откатил
`src/houseplan-card.ts` и `src/wall-face-repair.ts` к состоянию `2aaef12`,
пересобрал, скопировал бандл в `demo/srv/assets/` и повторно прогнал тот же
smoke — рабочее дерево затем восстановлено к `HEAD` и пересобрано, `git
status` чист.
- на коде до фикса `ambiguousLargeGapUsesWallsFlow` → **`false`** (упал), все
остальные 15 проверок остались `true`. Это прямое доказательство того, что
проверка M2 фальсифицируема и действительно закрывает найденный дефект, а не
переформулирует его условие.
- `hostedOpeningBlocksRepair` на коде до фикса остался `true`: ожидаемо — сам
инлайновый guard (`if (proposal.sourceKey.startsWith('static:partition|'))
…`) в `2aaef12` был по чтению кода уже корректен, M1 была находкой про
**отсутствие доказательства**, а не про баг в логике (это прямо
сформулировано в M1 из r1: «Guard … по чтению кода верен … Но ни один тест
не воспроизводит этот случай»). Для этой находки доказательство падения даёт
прямой unit-тест на извлечённую функцию: `repairMovesHostedPartition` в
`2aaef12` не существовала вовсе (`grep` по старому файлу — 0 совпадений),
поэтому импорт этой функции в `test/wall-face-repair.test.mjs` на коде до
фикса завершился бы ошибкой сборки/импорта, а не тихим прохождением. Тест
падать умеет, просто не через тот же smoke-сценарий, а через прямой unit.
**Чего не прогонял и почему:**
- `smoke_plan_snap_overlay.mjs`, `smoke_unified_wall_tool.mjs` — прогнаны в r1
зелёными (34/34, 19/19), дельта r1→r2 не касается ни snap-overlay, ни
strict-Shift кода; наследуются без повторного прогона (раздел 4).
- `npm run golden:verify`, полный smoke-набор (127 файлов), `pytest
tests_backend`, performance-профиль — как и в r1: diff не меняет ни один
существующий видимый кадр (правки чинят внутреннюю ветку без нового
рендера), ни один `.py`-файл не тронут, задача не задевает все поверхности.
Полные наборы — предрелизный гейт (§8 PROCESS.md), не гейт ревью; причины не
изменились с r1 и заново не переоцениваю их для дельты, которая эти
поверхности не трогает.
## 3. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** — AC7 требует `unit + smoke` для «invalid hosted opening», guard в `_validateWallRepair` (houseplan-card.ts:12673, на SHA `2aaef12`) существовал, но не был воспроизведён ни одним тестом | Guard вынесен в именованную экспортируемую чистую функцию `repairMovesHostedPartition` (`src/wall-face-repair.ts:29-40`), покрытую прямым unit-тестом с тремя ветвями (совпадающий host, несовпадающий host/kind, не-partition `sourceKey`); плюс сквозной smoke-сценарий `hostedOpeningBlocksRepair`, который проводит `_offerExistingWallFace` → `_saveRoom()` через реальный hosted-opening на дистанции 1,2 см и проверяет, что `_curSpaceCfg` не изменился и показан именно `toast.wall_repair_changed` | `src/wall-face-repair.ts:29-40` (функция); `test/wall-face-repair.test.mjs:53-70` (unit, 3 ветви); `demo/smoke_plan_drawing_repairs.mjs:104-118` (smoke); вызов на Create-пути — `src/houseplan-card.ts:12674-12676` внутри `_validateWallRepair`, дёргается из `_applyWallFaceBatch` (`:12777`) |
| **M2** — широкая (>2 см, screen-scale) диагностика в `_offerExistingWallFace` не различала `diagnostic.kind === 'repair'` и `'ambiguous'`; при ambiguous клик перехватывался и подсвечивался произвольный `proposals[0]` вместо обычного Walls flow (§8.6 ТЗ) | Условие сужено с `if (diagnostic.kind !== 'none')` до `if (diagnostic.kind === 'repair')`; при `'ambiguous'` код больше не входит в ветку, падает через к `if (!face …) return false;` — обычный Walls flow, без toast, без подсветки, без перехвата клика | `src/houseplan-card.ts:7684` (изменённое условие); regression-сценарий `ambiguousLargeGapUsesWallsFlow` в `demo/smoke_plan_drawing_repairs.mjs:94-102`, воспроизведён как падающий на коде до фикса (раздел 2 выше) |
Обе находки закрыты фактическим кодом и тестом, который умеет падать (M2 —
прямым воспроизведением регресса; M1 — прямым unit-тестом на извлечённую
функцию), а не заявлением автора. Хендофф автора
([issuecomment-5379081221](https://github.com/Matysh/houseplan-card/issues/228#issuecomment-5379081221))
совпадает с тем, что видно в дельте.
## 4. Унаследовано из r1
Без повторной проверки в этом раунде, полностью на основании
`docs/reviews/CODE-REVIEW-228-r1.md` (в дереве на SHA `9540627`,
предмет разбора там — SHA `2aaef12`, тот же код в этих AC дельтой r2 не
тронут):
- **AC1–AC4** (активная ось/узел на текущем отрезке, fail-closed на
неоднозначных endpoint, strict-Shift через точное пересечение луча, цвет
подписи по точному вектору) — `src/plan-snap-overlay.ts`, `src/logic.ts`
(`isExact45Vector`), рендер `.active-axis`/`.active-vertex`. Дельта r1→r2 не
трогает ни один из этих файлов.
- **AC5/AC8** (комната из готового контура, `Shift+click` bypass,
create-existing-face сохраняет партиции) — кроме уточнённой в M2 ветки
широкой диагностики, остальная логика `_offerExistingWallFace` и
`findWallFaceAtPoint`/`wall-face-graph.ts` не менялась.
- **AC9** (диалог удаления через `hp-dialog`, не `confirm()`) — `src/houseplan-card.ts`
вне тронутого дельтой диапазона строк, `src/room-deletion.ts` не в diff.
- **AC10–AC12** (Keep/Delete walls материализация и rehost, атомарность
Undo, fail-before-mutation порядок) — `src/room-deletion.ts` не в diff r2.
- **AC13–AC16** (View не регрессирует, touch/gesture guard не тронут, perf-путь
не заходит в hover, backend/schema не тронуты) — ни один из названных в r1
файлов (`src/houseplan-card.ts:_svgPointerMove`, `custom_components/**/*.py`)
не в diff r2.
- **AC17** (гейты/i18n/changelog/документация уровня r1) — новые i18n-ключи в
дельте не добавлялись (использован уже существующий `toast.wall_repair_changed`,
проверено `grep` — есть в обоих `src/i18n/*.json` и не создан в этом диффе);
changelog RU+EN обновлён именно в фикс-коммите `172d5d7` с точными формулировками
под M1/M2 (см. раздел 5).
Основание для наследования: дельта `2aaef12..172d5d7` не задевает ни один файл
и ни одну строку, от которых зависят перечисленные AC — проверено `git diff
--stat` (раздел 1) и точечным чтением затронутых строк (раздел 3). Это не
«не проверялось», а «проверено в r1 на коде, который здесь не менялся».
## 5. Дополнительно проверено в этом раунде
- Оба changelog (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в том же
коммите `172d5d7`, что и код, с `User-Visible: yes` — трейлеры на коммите
корректны (`Issue: #228`, `User-Visible: yes`). Текст обеих правок точно
называет оба закрытых поведения: «ambiguous wider gaps remain ordinary
drawing» (M2) и «a wall carrying a hosted opening is never moved by repair»
(M1) — формулировки не обобщают и не расходятся с кодом.
- `repairMovesHostedPartition` вызывается из `_validateWallRepair`, которая
используется на **обоих** путях подтверждения repair —
`_applyWallRepair` (одиночный, `:12685`) и `_applyWallFaceBatch` (через
`repairs[0]`, `:12777`) — оба реально достижимы из `_saveRoom`/`_offerExistingWallFace`
флоу, guard не обходится ни одним из них.
- Оставление предложения (`_offerExistingWallFace`) hosted-репейра без
немедленного отказа и отказ только на Create — соответствует ТЗ §8.6
дословно («Room dialog не применяет repair заранее. Create повторно
проверяет … hosted openings»), это не новый пробел, а сознательный
двухфазный контракт, уже заложенный в ТЗ и подтверждённый в r1.
- Тестовый счётчик: r1 зафиксировал `1031 pass`, здесь `1032 pass` — разница
ровно на 1 новый unit-тест M1, без скрытых удалений/пропусков (`0 skipped`,
расходится с хендоффом автора «1 skipped», как и в r1 — этот разброс не
влияет на вердикт, оба прогона зелёные).
## 6. Чего не проверял
- Всё, что унаследовано из r1 (раздел 4) — не переисполнялось повторно; список
причин не изменился и заново не переоценивался, кроме подтверждения, что
соответствующие файлы вне дельты.
- `npm run golden:verify`, полный (127 файлов) smoke-набор,
`python -m pytest tests_backend`, performance-профиль — не запускал в этом
раунде; причины совпадают с r1 (раздел 2) и дельта их не меняет: новых
видимых кадров нет, `.py` не тронут, задача не задевает все поверхности.
Это предрелизный гейт (§8 PROCESS.md, §11.4 AGENTS.md), не гейт ревью.
- Многопользовательский конкурентный сценарий и touch-специфичные жесты для
repair-пути — не переисполнялись; вывод r1 («чтением, не исполнением») не
затронут дельтой, которая не трогает `_markupClick`-обёртку или
multi-client код.
## 7. Вердикт
Обе Medium-находки r1 закрыты кодом и тестом, способным падать; никаких новых
находок в дельте не обнаружено. High — 0, Medium — 0.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Готово к очереди на пре-релиз (`S8-merged`).