20 KiB
Ревью ТЗ — issue #138, цикл r1
- Issue: https://github.com/Matysh/houseplan-card/issues/138
- ТЗ:
docs/specs/138-adjacent-room-autoclose.md, коммит611c5a76f56ce83587eafbd00dbd6ad1a0f8c9d8 - Этап: spec (PROCESS.md §2.4)
- Вердикт: красный · цикл r1/4 · High: 1 · Medium: 0
Скоуп ревью
Прочитаны в указанном порядке: docs/SCOPE.md, AGENTS.md, PROCESS.md (§1,
§2, §3, §4, §7, §12), тело issue #138 и оба комментария владельца (аналитика
Q1–Q5 с defaults, подтверждение ТЗ), docs/USER-GUIDE.ru.md (терминология
диалога комнаты, кнопка «Оставить замкнутыми стенами», секции про соседние
комнаты и общую стену), и связанный docs/specs/137-plan-snap-overlay.md как
основа, на которой строится #138.
Задача не помечена small, ТЗ корректно лежит в docs/specs/138-adjacent-room-autoclose.md
и зарегистрировано в docs/specs/README.md:49. Формат ревью — полный документ,
не комментарий.
Как проверялось
Ревью читало не только текст ТЗ, но и код, на который оно опирается, чтобы отличить обоснованное техническое утверждение от догадки, выданной за факт:
src/houseplan-card.ts:_markupClick()(6454–6575),_closeRoomContour()(6426–6452),_resolvePlanDrawPoint()(6011–6031),_draftEndAt()(6580–6594) — сверка с §3 «Причина дефекта», §7.1 «Порядок разрешения клика», §8 «Контракт замыкания».src/plan-snap-overlay.ts(1–180): структураPlanSnapSegment,sourceKind: 'room'|'draft'|'partition',cutSegments/roomEdges, дедупликация поaxisKey— сверка с §7.2 «Допустимый существующий интервал» и терминологией «room-owned solid interval».src/wall-thickness.ts:461applyWallThicknessToNewRoom()— сверка с §9 (наследование толщины общей стены) и AC7.src/i18n/ru.json:9,267–269— существующиеbtn.keep_as_walls,toast.room_overlap,toast.contour_min_edges,toast.contour_cannot_close— сверка с заявлением «новых i18n-ключей не требуется» (§4.4, §6).docs/specs/README.md— регистрация spec-файла.
Каждое утверждение ТЗ о текущем поведении, которое удалось проверить чтением
кода, подтвердилось: терминология (sourceKind, _markupClick,
_closeRoomContour, _resolvePlanDrawPoint, applyWallThicknessToNewRoom,
«Оставить замкнутыми стенами») — не изобретена, а взята из реального кода.
Отдельно оценивалась полнота контракта: не пропущен ли граничный случай,
который спецификация не проговаривает и не относит явно в non-scope.
Находки
High-1 — контракт автозамыкания перехватывает и блокирует обычный второй клик по той же стене
Где: docs/specs/138-adjacent-room-autoclose.md §7.1 (строки 105–119),
§7.2 (121–139), §8.1 (155–173), §8.2 (175–186).
Формулировка: §7.1 п.3 требует проверять на автозамыкание любой
обычный клик, отличный от клика в собственную первую точку контура — без
условия «в контуре уже есть хотя бы одно нарисованное ребро». §7.2 требует
только геометрических условий (общий room-owned solid interval, содержит A
и финальную точку, положительная длина, точки различны) — тоже без условия на
уже накопленное число вершин. §8.1 требует минимум три различные вершины
у prospective polygon [..., P, B] + B—A, иначе (§8.2) диалог не открывается,
B/P—B/B—A не записываются, и клик не превращается в обычное
добавление B — то есть точка не появляется в drafting вообще, только
показывается existing validation toast.
Отсюда прямое следствие: если пользователь ставит первую точку A на
существующем углу (this._path = [A], houseplan-card.ts:6544), а второй
клик кладёт на другой конец той же самой прямой существующей стены (что
является совершенно естественным способом начать соседнюю комнату — сначала
отметить оба конца общей стены, а затем обвести остальной контур), —
P на этот момент равен A (в пути одна точка), и prospective polygon равен
[A, B] + B—A: две различные вершины, не три. Проверка §8.1 обязана
провалиться, и по букве §8.2 клик обязан быть отклонён с toast, а B —
не добавлен в draft вообще.
Сегодня, до этой задачи, тот же клик работает штатно: _markupClick() не
делает такой проверки и просто добавляет B как обычную точку контура
(houseplan-card.ts:6573–6574, путь this._path = [...this._path, pt]; this._persistActiveDraftSegment();). Показательно, что для симметричного
случая — клика в собственную первую точку — система уже сегодня специально
избегает этой ловушки: строка houseplan-card.ts:6522
(const closing = this._path.length >= 3 && this._samePt(pt, this._path[0]);)
трактует клик как попытку замкнуть контур только когда уже накоплено
достаточно вершин; при недостатке вершин точка добавляется как обычная —
ошибка «нужно минимум два ребра» никогда не показывается на пустом контуре.
Новый контракт для автозамыкания (§7.1 п.3) не воспроизводит этот защитный
гейт: он назначает автозамыкание кандидатом для проверки независимо от того,
сколько вершин уже нарисовано, а §8.2 явно запрещает деградацию к обычному
добавлению точки при провале.
Сценарий воспроизведения (по тексту ТЗ, до реализации — логическая проверка контракта):
- Существует завершённая комната с прямой стеной
A—C(общая длина,AиC— её концы). - Пользователь выбирает «Контур комнаты», кликает в
A—this._path = [A]. - Следующим кликом отмечает
C— противоположный конец той же стены, намереваясь провести по ней первую грань новой комнаты, а затем обвести остальной периметр и замкнуть контур явным кликом вA(существующий, неизменяемый способ, приоритет 1 из §7.1). - По §7.1 п.3 клик в
Cне равен_path[0]=A, не является Ctrl/Cmd — значит проверяется как кандидат на автозамыкание. По §7.2AиCлежат на одномsourceKind: roomсплошном интервале, точки различны, длина положительна — eligibility выполнена. - По §8.1 prospective polygon
[A, C] + C—Aсодержит 2 различные вершины, что меньше требуемых трёх → невалиден. - По §8.2 диалог не открывается,
Cне добавляется в draft, показывается toast «Чтобы замкнуть контур, сначала нарисуйте минимум две грани» (toast.contour_min_edges), а клик не превращается в обычное добавление точки. - Результат: пользователь не может продолжить рисовать — второй клик по общей стене, который сегодня работает и является естественным способом начать примыкающую комнату, теперь ничего не делает, кроме показа сообщения об ошибке, не имеющего отношения к тому, что человек пытался сделать.
Это прямая регрессия текущего, работающего поведения — причём именно в той области (рисование вдоль общей стены соседней комнаты), которую задача #138 должна улучшить, а не ухудшить. Ни один AC (§13) и ни один пункт плана unit-тестов (§14.1, пп.1–9) не покрывает этот случай: пункт 7 («одинаковые A/B, zero-length и current anchor не подходят») — про совпадающие точки, а не про две различные точки на одном интервале при пустом ранее контуре. Риск не упомянут и в таблице §17.
Почему это High, а не Medium: правило §3.8 и §2.4 требует, чтобы контракт поведения был однозначным и не содержал воспроизводимой логической ошибки; здесь ошибка выводится напрямую из текста ТЗ без домысливания реализации, и затрагивает основной, ежедневный workflow редактора (первые же клики при разметке соседней комнаты — ровно сценарий из тела issue). Это не пограничный кейс, а типичный способ начать рисовать общую стену.
Что нужно поправить в ТЗ (не решение реализации, а контракт):
Автозамыкание должно проверяться только когда в контуре уже достаточно
вершин, чтобы в принципе замкнуться (аналогично существующему гейту
this._path.length >= 3 для клика в первую точку — т.е. до клика в B уже
должно быть отрисовано хотя бы одно дополнительное ребро, не лежащее
целиком на том же интервале что и A—B), либо явно указать, что при провале
именно по причине «меньше трёх вершин» (в отличие от self-intersection/overlap/
zero-area) клик деградирует к обычному добавлению точки, а не блокируется
toast'ом. Выбор между этими двумя вариантами — продуктовый (что видит
пользователь при клике по второй точке общей стены сразу после первой):
либо клик тихо добавляет обычную точку (как сегодня), либо показывает
ошибку. Это ровно тот вопрос, который стоило задать владельцу вместе с Q1–Q5,
и он остался незамеченным.
Что проверено и корректно
- Терминология и техническая база не выдуманы. Все ссылки на
_resolvePlanDrawPoint,_markupClick,_closeRoomContour,_draftEndAt,_persistActiveDraftSegment,sourceKind: 'room'|'draft'|'partition',applyWallThicknessToNewRoom,plan-snap-overlay.ts, кнопку «Оставить замкнутыми стенами» и существующие toast-ключи — соответствуют действительному коду. §3 «Причина дефекта» описывает код, каким он есть сегодня, а не предположение. - Обязательные разделы §7.1 PROCESS.md на месте: сценарий/персона/ поверхность (§1), «что человек увидит до/после» одной фразой без терминов реализации (§2), проблема (§3), scope/non-scope (§5–6), контракт поведения (§7–9), UX и touch-деградация (§10), модель данных/миграция (§11), AC1…AC12 с указанием способа доказательства (§13), план автотестов (§14), риски (§17), откат (§18), release-артефакты (§16).
- i18n корректно закрыт: новых ключей нет, переиспользуются существующие
toast.contour_min_edges,toast.contour_cannot_close,toast.room_overlap,btn.keep_as_walls— подтверждено чтениемsrc/i18n/ru.json/en.json. - Наследование толщины общей стены (AC7, §9) грамотно опирается на уже
существующий
applyWallThicknessToNewRoom()(wall-thickness.ts:461–484), который и сегодня пропускает интервалы с уже ненулевой толщиной (cms[i] > 0) — новый код для этого не нужен, утверждение ТЗ верно. - Проёмы и cuts (§7.3, AC4) корректно опираются на уже существующий
cutSegments/canonical cuts из #137 (plan-snap-overlay.ts), не вводят второй resolver — соответствует Non-scope и архитектурному контракту §12. - Дедупликация общей стены между двумя комнатами (§9, «не создаёт вторую
физическую стену») согласуется с найденной в
plan-snap-overlay.tsдедупликацией сегментов поaxisKey(одинPlanSnapSegmentна общую ось независимо от числа комнат-источников) — заявление обосновано, не догадка. - Открытых продуктовых вопросов действительно не осталось для того, что было явно задано: Q1–Q5 из комментария аналитики и все технические defaults подтверждены владельцем 2026-08-14 (issue-комментарий), ссылка есть в ТЗ §4 и §19.7. «Ни одного открытого вопроса» здесь не является тревожным сигналом самим по себе — вопросы были заданы и закрыты пакетом, а не обойдены.
- Release-артефакты и трек соответствуют процессу:
User-Visible: yes, оба changelog в одном коммите,docs/CANVAS.md/ARCHITECTURE.md/USER-GUIDE.ru.mdв списке правок (§16), полный трек (неsmall/trivial) обоснован в аналитике — сложность и число задетых инвариантов оправдывают выбор. - Non-scope сформулирован конкретно и исключает реальные соблазны расширения (путь по нескольким рёбрам, второй resolver проёмов, новая визуальная индикация, hit-radius #137) — не оставляет скрытого расширения скоупа.
Чего не проверял
- Реализацию — её ещё нет; это ревью ТЗ, а не кода (PROCESS.md §2.4 не предполагает исполнения на этом этапе).
- Golden/визуальные артефакты — ТЗ корректно утверждает, что новых пикселей нет (§14.3), баз для сверки нет и не требовалось.
- Производительность вживую — утверждение об O(S)-поиске только на click
(§12, AC10) проверено чтением архитектуры snap-геометрии (кэшированный
snapshot в
_planSnapGeometrySnapshot()), не профилированием — на этапе ТЗ профилирование не требуется. - Backend/schema — ТЗ утверждает отсутствие изменений (§11, AC12); код
custom_components/houseplan/**/*.pyне затрагивается по scope ТЗ, отдельно не сверялся построчно, поскольку заявленный scope его не касается.
Вывод
Один блокирующий (High) дефект контракта: связка §7.1 п.3 + §7.2 + §8.1 + §8.2 без защитного гейта по минимальному числу уже нарисованных вершин превращает обычный, сегодня работающий второй клик по общей стене в блокирующую ошибку — регрессия ровно того сценария, который issue #138 должен исправить. Остальной контракт, включая проёмы, толщину, drafts/partitions, historyCancel/Save, touch-деградацию и release-артефакты, проверен по коду и корректен.
Возврат в «ТЗ в работе» (S3-spec), цикл r1/4. Исправление: явно решить (и
записать в ТЗ), что происходит при клике на другую точку того же интервала,
когда в контуре ещё недостаточно вершин для валидного замыкания — деградация к
обычному добавлению точки (по аналогии с текущим гейтом path.length >= 3 для
закрытия по первой точке) либо иной явно обоснованный вариант, покрытый
отдельным AC и unit-тестом.