28 KiB
CODE-REVIEW-228-r3
- Issue: #228 — надёжное рисование стен и операции с готовым контуром
- ТЗ:
docs/specs/228-plan-drawing-problems.md(ревью ТЗ зелёное, r1, #228) - Ветка:
issue/228-plan-drawing-problems - Коммит на момент этого захода:
55d5a56(HEAD; продуктовые коммиты в диапазоне —2f96899"fix: make plan drawing fail closed",691cea0"fix: close plan repair review findings") - Заход ревью: r3 · блокирующих циклов израсходовано 1 из 4 (r1 был жёлтым и списал единицу; r2 был зелёным и бюджет не потратил, #227)
- Ревьюер: Claude (код-ревью), сессия без контекста реализации и без контекста r1/r2-ревью
0. Предыдущий раунд, SHA и причина полного разбора
Вердикт r2 найден в комментарии issue
(issuecomment-5379111358):
зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0, на SHA 172d5d7
(явно назван в шапке docs/reviews/CODE-REVIEW-228-r2.md). Готово к очереди на
пре-релиз.
Сразу после зелёного r2 слияние в dev не удалось: ветка issue/228-plan-drawing-problems
конфликтовала с dev, куда параллельно вошла работа по #233 («measure resize
labels between wall faces», коммиты abfaae3/bff47f5/…/66ffd6f). Задача ушла
в S6-in-progress не на правку кода, а на ребейз (комментарий
issuecomment-5379112319).
Автор перебазировал ветку на актуальный origin/dev (66ffd6f), вручную
объединив RU/EN changelog и пересобрав три копии бандла; продуктовый TypeScript
слился автоматически без конфликтов (комментарий
issuecomment-5379127615).
Автоматический прогон ревью после этого не отработал (упавший workflow run);
метка осталась на месте, что по §10.4 AGENTS.md означает сбой самого прогона, а
не вердикт — правки в задаче нет, ревью просто не состоялось. Этот заход и есть
тот прогон.
git rev-parse HEAD = 55d5a560adfc1da92f774b4073cbd37065de4f66,
git merge-base HEAD origin/dev = 66ffd6fdda347a005993a8d396ee61c14ccc3adc = origin/dev —
то есть ветка полностью содержит текущий dev. Старые SHA 2aaef12/172d5d7
после ребейза недостижимы (переписаны, как и ожидается от git rebase).
Это ребейз на ушедший вперёд dev — по прямому указанию задачи разбор в этом
заходе полный (git diff origin/dev...HEAD), а не по дельте r2→r3: после
ребейза это, формально, другой код (§7.2 AGENTS.md), и есть конкретный риск —
и #228, и #233 правят один и тот же файл src/houseplan-card.ts (#233
затрагивает секцию «room resize tool» вокруг _rszInnerSpanCms, #228 — секции
Walls/room-markup/room-deletion/wall-thickness), поэтому нужно было убедиться,
что автослияние не потеряло и не задвоило код ни одной из задач.
1. Скоуп проверки — полный диапазон
git log --oneline origin/dev..HEAD: 6 коммитов — e44b3c9 (ТЗ), 9a131cc
(SPEC-REVIEW r1), 2f96899 (реализация r1, бывший 2aaef12), 8fc3ec7
(CODE-REVIEW r1), 691cea0 (фикс r2, бывший 172d5d7), 55d5a56
(CODE-REVIEW r2). Трейлеры на обоих продуктовых коммитах корректны: Issue: #228,
User-Visible: yes, оба меняют docs/CHANGELOG.md/docs/CHANGELOG.ru.md в том
же коммите.
git diff origin/dev...HEAD --stat: 33 файла, +3190/−356. Продуктовый код —
src/houseplan-card.ts (+396/−54 фактически, часть строк — импорты/поля),
src/plan-snap-overlay.ts (+178/−26 с учётом рефакторинга resolvePlanSnap в
resolvePlanSnapResult), новые src/wall-face-repair.ts (+165),
src/room-deletion.ts (+122), расширение src/wall-face-graph.ts (+36),
src/logic.ts (+12, isExact45Vector), src/styles.ts (+27). Тесты — четыре
файла (+166 суммарно), новый smoke demo/smoke_plan_drawing_repairs.mjs
(172 строки) плюс точечная правка demo/smoke_unified_wall_tool.mjs. Семь
файлов документации, RU/EN i18n, три синхронные копии бандла, tsconfig.test.json.
Прочитаны перед разбором: docs/SCOPE.md, AGENTS.md, PROCESS.md, тело issue
#228 и все 13 комментариев (аналитика → продуктовые вопросы → решения
владельца → «ТЗ готово» → зелёное ревью ТЗ → «Взял» → хендофф r1 → жёлтый r1 →
хендофф фикса → зелёный r2 → неудавшееся слияние/ребейз → хендофф ребейза →
сбой автопрогона), полный текст docs/specs/228-plan-drawing-problems.md (все
17 AC), оба предыдущих документа код-ревью (CODE-REVIEW-228-r1.md,
CODE-REVIEW-228-r2.md), затронутые канонические документы (CANVAS.md,
ARCHITECTURE.md, WALL-THICKNESS.md, TOUCH-SUPPORT.md, USER-GUIDE.ru.md),
а также коммиты #233 (abfaae3 и соседние) — чтобы понять зону пересечения
файлов при автослиянии.
2. Как проверялось — гейты
Объём гейтов соразмерен задаче (§8 PROCESS.md): дешёвые прогнаны полностью, из тяжёлых прогнаны именно те, что закрывают риск ребейза (пересечение с #233) и названы в АС; остальные — предрелизный гейт.
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный, без вывода |
| Unit | npm test |
# tests 1036 / # pass 1036 / # fail 0 (совпадает с npm run inventory: Node unit 1036) |
| 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 (АС #228) | node demo/smoke_plan_snap_overlay.mjs |
34/34 true, OK |
| Named smoke (АС #228) | node demo/smoke_unified_wall_tool.mjs |
19/19 true, OK |
| Named smoke (АС #228) | node demo/smoke_plan_drawing_repairs.mjs |
16/16 true, OK (включая hostedOpeningBlocksRepair и ambiguousLargeGapUsesWallsFlow — оба фикса r1/r2 продолжают проходить после ребейза) |
| Smoke пересекающейся задачи (риск ребейза) | node demo/smoke_resize_inner_dimensions.mjs |
OK — #233 использует тот же src/houseplan-card.ts; зелёный прогон подтверждает, что автослияние не задело код измерения resize-подписей |
Чего не прогонял и почему:
npm run golden:verify— diff не меняет ни один существующий видимый кадр: новые визуальные элементы (.active-axis,.active-vertex,.wall-repair-preview,.plan-snap-node.conflict,hp-dialogудаления комнаты) рендерятся только при активном инструменте/диалоге, что подтверждаетviewHasNoEditorRepairChromeв смоке. Полная golden-матрица (включая forced-colors для нового.conflict-состояния, см. §6) — предрелизный гейт (§14.3 ТЗ, §8 PROCESS.md), не гейт код-ревью; это же решение принято в r1/r2 и дельта его не меняет.python -m pytest tests_backend— ни один.py-файл не тронут ни в диапазоне #228, ни в объединяющем ребейзе; ТЗ §16 прямо фиксирует отсутствие backend/schema изменений, подтвержденоgit diff --name-only.- Полный smoke-набор (163 файла по
npm run inventory) и performance-профили — задача не задевает все поверхности; названные в АС и связанные с диапазоном ребейза (smoke_resize_inner_dimensions) прогнаны выше. Предрелизный гейт по действующему решению владельца (§8, §11.4 AGENTS.md).
3. Ребейз: что именно проверено дополнительно к r1/r2
Поскольку разбор полный, ниже — не пересказ r1/r2, а свежая проверка того, что
могло сломаться именно ребейзом (автослияние .ts, ручное слияние changelog,
пересборка бандла), плюс сквозное чтение всего diff'а заново.
- Пересечение файлов с #233.
git diff#233 (abfaae3) правитsrc/houseplan-card.tsтолько вокруг_rszInnerSpanCms/_rszScaleLabels(секция «room resize tool», строки ~8248–8340 до ребейза) и новыйsrc/wall-thickness.ts. Диапазоны, которые правит #228 вhouseplan-card.ts(импорты,_deleteRoomClick/_confirmRoomDeleteв районе строки 11053,_validateWallRepair/_applyWallRepairв районе 12704–12780, рендер в районе 16780/18840/19078) не пересекаются со строками #233. Это подтверждает, почему TypeScript-слияние прошло автоматически без конфликта, а не просто «повезло»: зоны правок дизъюнктны. - Отсутствие маркеров незакрытого конфликта.
grep -n "<<<<<<<\|=======\|>>>>>>>"по всем изменённым.ts-файлам (houseplan-card.ts,wall-thickness.ts,wall-face-graph.ts,wall-face-repair.ts,room-deletion.ts,plan-snap-overlay.ts,logic.ts,styles.ts) — 0 совпадений (найденные строки=================— это секционные комментарии-разделители кода, не следы конфликта). - M1/M2 из r1, закрытые в r2, физически присутствуют после ребейза.
grep:repairMovesHostedPartitionопределена вsrc/wall-face-repair.ts:34-42, используется в_validateWallRepair(src/houseplan-card.ts:12717-12719), покрыта вtest/wall-face-repair.test.mjs. Условие M2 (diagnostic.kind === 'repair', а не!== 'none') стоит наsrc/houseplan-card.ts:7685внутри_offerExistingWallFace(:7656–7695) — единственная ветка широкой (>2 см) диагностики,ambiguousбольше не перехватывает клик. - Ручное слияние changelog не задвоило и не потеряло записи.
docs/CHANGELOG.md/docs/CHANGELOG.ru.mdсодержат отдельными абзацами и #228, и #233 (плюс #234, #230 из более раннегоdev), без дублирования текста и без обрыва предложений — прочитаны оба файла целиком. - Бандл собран, а не просто перенесён. Свежий
npm run buildпосле ребейза даёт файл, побайтно совпадающий с уже закоммиченными тремя копиями (§2), иgit statusпосле сборки чист — то есть коммит691cea0содержит именно результат сборки текущего исходника, а не бандл, оставшийся от до-ребейзного состояния.
Все пять пунктов подтверждают: ребейз чисто механический, продуктовое поведение #228 не изменилось относительно уже дважды проверенного r1→r2 кода, и он не повредил параллельно вошедшую работу #233.
4. Разбор по AC (полный, не только дельта ребейза)
- AC1 (активная ось/узел) —
.active-axis/.active-vertex(src/houseplan-card.ts:19012-19024) рендерятся изthis._cursorPtповерх толстого preview, узел скрывается, когда есть активный snap-candidate или конфликт (!this._activePlanSnapCandidate && !this._activePlanSnapConflicts.length) — не дублирует существующий маркер.smoke: activeSegmentShowsAxisAndNodeзелёный. Проверено автотестом и чтением. - AC2 (неоднозначные endpoints) —
endpointResolution()(src/plan-snap-overlay.ts:296-321) даёtambiguous, когда ≥2 разных ключа оказались внутриdistinguishTolerance; click-путь (_markupClick,houseplan-card.ts:7286-7295) наresolved.ambiguousпоказывает toast иreturnбез записи точки/истории/конфига.test/plan-snap-overlay.test.mjsиsmoke: closeEndpointsFailClosedзелёные. Проверено автотестом. - AC3 (strict Shift) —
resolveStrictPlanSnap(src/plan-snap-overlay.ts:324-378) проецирует на луч, кратный 45° отanchor, для endpoint и line/segment intersection через параметрическое уравнение; non-Shift путь (resolvePlanSnapResult) не меняет прежнее поведение кроме ambiguity guard. Unit +smoke: strictShiftUsesExactRayIntersectionзелёные. - AC4 (угловая подпись) —
isExact45Vector(src/logic.ts:1937-1946) заменилis45(deg, 0.5)(houseplan-card.ts:18127), сравниваетdx/dyс эпсилон, а не округлённые градусы.smoke: angleColourMatchesActualVectorявно проверяет 90,1° как красный. - AC5/AC8 (комната из готовой области) —
findWallFaceAtPoint(src/wall-face-graph.ts:571-591) — point-in-polygon с исключением границы и выбором наименьшей площади при равенстве поkey; вызывается из_offerExistingWallFace(houseplan-card.ts:7656) только при пустом_path, без Shift и без snap-candidate (условие наhouseplan-card.ts:7316). Decline (Keep/Cancel) для одногоexisting/repair-кандидата — явный no-op (houseplan-card.ts:12624-12631, ничего не пишет вsp). Smoke:existingFaceOffersRoom,keepExistingFaceIsNoop,shiftBypassesExistingFaceOffer,createExistingFaceKeepsPartitions— зелёные. - AC6/AC7 (repair ≤2 см, атомарность, негативная матрица) —
planWallFaceRepair(src/wall-face-repair.ts:101-165) — чистая функция, двигает только independent endpoint (movable()исключаетstatic:room|), выбирает детерминированный mover поstableEndpointMover(grid-canonicality, затем геометрический tie-break, не record order)._validateWallRepair(houseplan-card.ts:12704-12720) ревалидирует source/target по актуальному графу и отклоняет repair, двигающий partition с hosted opening, черезrepairMovesHostedPartition. Негативная матрица (>2 см, two-gap→ambiguous, room-vertex immovable, hosted opening) покрыта unit-тестами вtest/wall-face-repair.test.mjsи smoke-сценариямиhostedOpeningBlocksRepair/ambiguousLargeGapUsesWallsFlow/smallGapOffersRepair. Обе находки r1 (M1/M2) остаются закрыты — см. §3.4. - AC9 (диалог удаления) —
_renderRoomDeleteDialog(houseplan-card.ts:19078-19100) использует<hp-dialog>(неconfirm()), primary «Delete room, keep walls», danger «Delete room and walls»,@hp-closeна Cancel/X/Escape.smoke: deleteUsesAccessibleChoiceDialogподтверждает отсутствиеconfirmи наличиеhp-dialog.hp-dialog— уже принятый переиспользуемый компонент (тот же, что и_renderPartitionDeleteDialog), его focus-trap/Escape-контракт вне diff'а — проверено чтением, не переисполнено отдельно (как и в r1). - AC10/AC11 (Keep/Delete walls) —
planRoomDeletion(src/room-deletion.ts:73-112) — чистая функция:exclusiveфильтруетkind === 'outer' && !open && cm > 0(позитивная толщина, не shared, не open span);containsIntervalпереиспользует совпадающую partition по углу+толщине+покрытию, не создавая дубликат. Openings сhost.kind === 'partition'исключены из переассоциации заранее — уже привязанный проём никогда не становится «комнатным». Caller (_confirmRoomDelete,houseplan-card.ts:11065-11123) считает capacity (MAX_PARTITIONS) по новым (не reused) партициям до_geometrySnapshot()/мутации (houseplan-card.ts:11089) — fail-before-mutation соблюдён. SmokekeepWallsMaterializesAndRehosts/deleteWallsCascadesExclusiveOpeningзелёные. - AC12 (один Undo/атомарность) —
_recordGeometry(...)вызывается один раз в конце_confirmRoomDelete(после_commitOpenSpans()/нормализации стен) и один раз в конце_applyWallFaceBatch; в обоих путях_validateWallRepair/ capacity-проверка выполняются до захватаbefore-снапшота. Проверено чтением (нет отдельного Undo/Redo unit-теста на этот путь, структура идентична уже принятому_applyWallFaceBatchиз #173). - AC13 (существующие рендер-потребители) —
git diff --name-onlyне содержит ни одного изо/sun/light-файла; новые SVG-элементы гейтятся текущим инструментом.viewHasNoEditorRepairChromeподтверждает отсутствие лишнего DOM в View. - AC14 (touch/gesture safety) — новых touch-путей нет;
_offerExistingWallFaceвызывается из того же_markupClick, что уже проходит через непереписанный suppressed-click/pointercancel guard. Проверено чтением, не исполнением (как и в r1 — отдельного targeted-смока на жест именно для existing-face/repair нет). - AC15 (perf) —
grepподтверждает:findWallFaceAtPoint/planWallFaceRepairвызываются только из_offerExistingWallFace(idle click,:7316) и_offerWallFaces(после принятого сегмента,:7588); ни одного вызова из_svgPointerMove/hover-пути. Проверено чтением; performance-профиль — предрелизный гейт по ТЗ §14.3. - AC16 (backend/schema) —
custom_components/**/*.pyне тронут ни в диапазоне #228, ни объединяющим ребейзом; новых persisted-полей нет. - AC17 (гейты/i18n/changelog/документация) — таблица §2; en/ru ключи
парные (
btn.delete_room_*,history.delete_room_*,confirm.delete_room_*,toast.plan_snap_ambiguous,toast.wall_repair_*) — свереноgit diffобоихi18n/*.jsonцеликом, для каждого нового en-ключа найдена ru-пара и наоборот; оба changelog правлены вUser-Visible: yesкоммитах2f96899/691cea0;docs/CANVAS.md,docs/ARCHITECTURE.md,docs/WALL-THICKNESS.md,docs/TOUCH-SUPPORT.md,docs/USER-GUIDE.ru.md,docs/TESTING.md,docs/STATUS.mdобновлены и не противоречат коду ни в одном прочитанном месте.
5. Что проверено и корректно (сверх раздела 4)
- Ключи
static:*/active:*/static:draft|<id>:<index>строятся в одном месте и корректно парсятся обратно в_applyWallRepair(houseplan-card.ts:12724-12772) для всех трёх веток (active:,static:partition|,static:draft|), включая сдвиг индекса+1дляendpoint === 'b'. Не нашёл рассинхронизации. stableEndpointMover/movable()(wall-face-repair.ts:44,68-81) исключает пары, где обе стороны — room-vertex, и не даёт room-полигону становиться «двигателем» даже когда он геометрически удобнее.repairMovesHostedPartitionвызывается на обоих путях подтверждения repair —_applyWallRepair(одиночный) и через_validateWallRepairвнутри_applyWallFaceBatch(houseplan-card.ts:12820-12823,repairs[0]) — guard не обходится ни одним из них.- Три копии бандла после чистой пересборки побайтно идентичны, рабочее дерево чисто после сборки — обязательное условие §8 AGENTS.md выполнено этим прогоном заново, на текущем (пост-ребейзном) SHA, а не унаследовано с r1/r2.
docs/reviews/CODE-REVIEW-228-r1.mdи-r2.mdв дереве соответствуют описанным в них SHA (2aaef12→2f96899,172d5d7→691cea0после ребейза); их аналитика по неизменным файлам (snap-overlay, strict-Shift, delete-room dialog, room-deletion) остаётся применимой, что и подтверждено сквозным чтением в §4 этого документа заново, а не голой ссылкой.
6. Чего не проверял
- Golden/визуальную матрицу (active axis, conflict-узел, red repair preview,
delete-dialog, включая forced-colors-контраст нового
.plan-snap-node.conflict, который не входит в текущий forced-colors smokedemo/smoke_plan_snap_overlay.mjs:242-254— тот проверяет только базовый.plan-snap-node/.plan-snap-line) — предрелизный гейт, не гейт код-ревью; это то же самое решение, что и в r1/r2 (там же явно зафиксировано «просмотрено только по исходнику styles.ts, не по скриншоту»), дельта ребейза его не меняет и новых визуальных классов не добавляет. - Полный HA backend harness — не тронут ни один
.py-файл. - Performance-профиль 60-room/60-partition (AC15) — не запускал профилировщик; вывод сделан чтением вызовов (§4, AC15). Предрелизный гейт по ТЗ §14.3.
- Touch-специфичные жесты (pinch/pan/pointercancel) именно для
existing-face/repair путей — не находил отдельного targeted smoke под палец;
полагался на то, что путь проходит через общий
_markupClickgesture-guard, не тронутый диапазоном #228 (то же основание, что в r1). - Многопользовательский конкурентный сценарий на delete/repair — не
воспроизводил вручную; по чтению кода план строится заново из текущего
spaceв момент подтверждения, а не из снимка на момент открытия диалога. - Полный smoke-набор (163 файла) сверх пяти прогнанных выше (три названных в
АС #228, один общий forced-colors и один —
smoke_resize_inner_dimensions— специально ради риска ребейза с #233); остальные 158 не запускал, диапазон их не задевает.
7. Вердикт
Ребейз на ушедший вперёд dev потребовал полного разбора вместо разбора по
дельте (§2.10/§7.2). Полный разбор не нашёл новых находок: обе Medium-находки
r1 остаются закрыты тем же кодом и тестами, что подтвердил r2, автослияние с
параллельной задачей #233 не пересекается по строкам и не повредило ни одну из
двух задач (подтверждено гейтами, целевыми и кросс-задачными smoke-прогонами и
сквозным чтением всего diff'а). High — 0, Medium — 0.
Вердикт: зелёный · заход r3 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Готово к очереди на пре-релиз (S8-merged).