19 KiB
SPEC-REVIEW-383-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/383
- Этап: ревью ТЗ (PROCESS.md §2.4)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (полный трек, лимит 4)
- Материал:
docs/specs/383-furniture-transform.md, ревизия 1, коммитaa1b9e39(docs: specify furniture transforms), веткаissue/383-furniture-transform. - Вердикт: зелёный
Скоуп ревью
Первый заход — разбор полный, дельты нет. Проверялись: соответствие
docs/SCOPE.md, наличие всех обязательных разделов §7.1, однозначность и
доказуемость каждого AC1–AC13, отсутствие непомеченных догадок о поведении,
согласованность с каноническими документами подсистемы (CANVAS.md,
FURNITURE.md, UX-MODES.md, CONFIG-COMPATIBILITY.md, TOUCH-SUPPORT.md,
USER-GUIDE.ru.md) и — там, где ТЗ описывает текущую реализацию как базу для
дельты — сверка этих утверждений с фактическим кодом на aa1b9e39.
Как проверялось
- Прочитаны
docs/SCOPE.md,AGENTS.md,PROCESS.mdцеликом. - Прочитано тело issue #383 и все шесть комментариев (аналитика, два решения
владельца по вопросам, уточнение про галочки отражения, взятие в работу,
готовность ТЗ). Открытых продуктовых вопросов на момент ревью нет — владелец
явно закрыл оба вопроса аналитики (Q1: свободный поворот без
Shift, 45° сShift; Q2: crossing через ноль разрешён; Q3: расширение hit-area на физические 10 см от штриха, а не весь bounding box). - Прочитан
docs/specs/383-furniture-transform.mdцеликом. - Прочитаны канонические документы:
docs/FURNITURE.md, фрагментыdocs/CANVAS.md,docs/UX-MODES.md,docs/TOUCH-SUPPORT.md,docs/CONFIG-COMPATIBILITY.md, соответствующие разделыdocs/USER-GUIDE.ru.md(мебель, толстые стены/invert, Optimize). - Технические утверждения ТЗ и аналитики сверены с кодом на
aa1b9e39(не для оценки реализации — реализации ещё нет, — а чтобы убедиться, что база, от которой считается контракт, не выдумана):resizeDecorBox()(src/editors/decor/geometry.ts:222) действительно получаетstep/minSizeи округляет обе оси кgridPitch— подтверждает «Проблема» и корректность выбранной base-line для AC1/AC3.- Общий поворот (
_dtMove,src/houseplan-editor-runtime.ts:4596-4601) сейчас округляет кDT_ANGLE_STEP(5°) безShiftи свободен сShiftдля всех decor kinds одной веткой — подтверждает нужность отдельной ветки для мебели (AC4) и корректность «остальные decor kinds без изменений». pointer-events: visiblePaintedна.dshapeв режиме decor (src/styles/plan.styles.ts:665-667) и увеличенный select hit-path.dselecthitтолько у<line>(plan.styles.ts:719-729,houseplan-card.ts:8130) — подтверждает «у мебели своего hit-path нет» и что образец для «прежнего hit contract» существует только для линии.- Существующий Save-обработчик свойств декора
(
houseplan-editor-runtime.ts:4459-4466) действительно вызываетsnapToGridдляrect|ellipse|furnitureодной веткой — AC5.5 корректно называет цель правки (убрать вызов только для furniture). - Дисплей/клэмп числового поля размера в диалоге свойств
(
_decorLargeField/_decorLargeCm,houseplan-card.ts:7618-7625) уже имеет пол 0,1 см — заявленный в ТЗ технический минимум (§3) не противоречит существующему клэмпу этого диалога. ОтдельныйFURN_MIN_CM=1вsrc/furniture.ts:309относится к другому пути — палитре новой мебели перед стемпом (_furnFieldToCm, используется только для_furnPalette.w/h), а не к диалогу свойств уже размещённого объекта; путаницы между двумя путями в тексте ТЗ нет. - Backend
_FURN_SIZE = vol.Range(min=0.0000001, max=CANVAS_LIMIT)(custom_components/houseplan/validation.py:1314) — совместим с новым 0,1-см полом (он строже, чем текущий backend-минимум, значит не открывает новый диапазон, отклонённый сейчас). flip_h/flip_vуже существуют в схеме — но дляopenings(validation.py:1631-1633,PASSAGE_FORBIDDEN_FIELDSline 728,import_export.py:280-284) с другим смыслом: не отражение размера, а направление створки/направление ворот (USER-GUIDE.ru.md:721-722, «Флаг „Открывается в другую сторону“»). Коллизии по факту нет — decor и openings обрабатываются раздельными циклами вcoordinate_canonicalization.py:143-159и раздельным allowlist-кодом вimport_export.py, поэтому смешения полей не будет. Отмечено ниже как Low-наблюдение, не блокирует.
docs/specs/README.md— задача добавлена; ссылки issue↔ТЗ на месте в обе стороны (issue → блоб файла в комментарии автора, файл → issue в шапке).
Проверка §7.1 (обязательные разделы)
Все обязательные разделы присутствуют и в правильном порядке: Сценарий (персона — Home admin, десктоп, Редактор подложки, момент — подгонка размера мебели под реальный объект) → Что человек увидит до/после (одной фразой, без терминов реализации) → Проблема → Скоуп/Не-скоуп → Контракт поведения (7 подсекций) → История/перенос/совместимость → UX/i18n → Затронутые файлы → Критерии приёмки AC1–AC13 с доказательством для каждого → План автотестов → Риски → Откат → Производительность и безопасность → Release-артефакты → Принятые предположения. Два продуктовых раздела не описывают реализацию — проверено.
Проверка AC1–AC13
Каждый AC — проверяемое утверждение с названным способом доказательства
(unit/smoke/golden/backend/DOM/integration/ручной скриншот, где это
уместно — AC8 сознательно называет «manual screenshot» для курсора, что
корректно: программно проверить визуальный SVG data-URI cursor можно только
косвенно). Ни один AC не описывает реализацию вместо наблюдаемого контракта.
Мутанты в «Плане автотестов» покрывают все 13 AC по одному-два мутанта на
критерий — сцепка AC↔мутант явная, а не общая фраза «тесты будут».
Не найдено ни одного AC, доказательство которого требовало бы теста, не умеющего упасть (например, AC3 «crossing и minimum» явно требует таблицы по осям/углам, а не общего «works»).
Проверка на непомеченные догадки
Единственные места, где ТЗ фиксирует поведение, которое не следует напрямую из
явного решения владельца, вынесены в «Принятые предположения» и помечены как
таковые (масштаб crossing на обеих осях одновременно; false как каноническое
отсутствие; точный ноль как невалидное transient-состояние; 10 см — от внешней
границы уже видимого stroke, а не от centerline). Все четыре — технические, не
продуктовые, и ревьюер вправе их принять или оспорить без обращения к
владельцу (PROCESS.md §7.1). Возражений по существу нет: все четыре
согласуются с §3/§6/§7 контракта и с уже принятым в проекте паттерном
«optional-флаг, absence = историческое поведение» (CONFIG-COMPATIBILITY.md,
многократно, напр. marker.value_source, space.zero_wall_style).
Продуктовых вопросов, которые следовало задать владельцу, но не задали, не найдено — оба вопроса из аналитики (модификатор поворота, поведение hit-area) уже закрыты явными решениями владельца в комментариях, а не додуманы автором.
Находки
Low-1 — i18n-ключи галочек отражения не в том namespace
furn.flip_h/furn.flip_v (спецификация, раздел UX/i18n) попадут в диалог
свойств уже размещённого объекта (там же, где decor.size, decor.angle,
decor.fill — все ключи этого диалога, включая существующие поля размера
мебели, уже используют namespace decor.*, см. src/i18n/ru.json:520-521 и
Save-обработчик houseplan-editor-runtime.ts:4459 — furniture лежит в одной
ветке с rect/ellipse). Namespace furn.* в проекте зарезервирован за диалогом
палитры новой мебели (furn.title, furn.width, furn.depth,
furn.pick_hint и т.д., src/i18n/ru.json:907-1002) — другой диалог, другой
момент взаимодействия. Ключи галочек стоит назвать decor.flip_h/
decor.flip_v, чтобы не заводить третий смешанный источник именования в одном
диалоге.
Почему Low, не Medium: чисто наименование, не влияет ни на один AC, чинится переименованием двух ключей в четырёх словарях без побочных эффектов.
Решение ревьюера: не блокирует, правится при реализации без возврата на
повторное ревью ТЗ; если автор оставит furn.* — тоже не дефект AC, только
стилистическая непоследовательность, которую тогда фиксируем без действия.
Low-2 — переиспользование имён flip_h/flip_v без ссылки на существующий смысл
Поля flip_h/flip_v уже существуют в backend-схеме для openings с иным
значением — направление створки двери/окна (docs/USER-GUIDE.ru.md:721-722,
validation.py:1631-1633). Раздел «Рекомендуемая модель» (комментарий
аналитики) и раздел 6 ТЗ вводят те же имена для мебели как будто с нуля, не
упоминая этот прецедент. Функциональной коллизии нет: decor и openings
обрабатываются раздельными циклами в coordinate_canonicalization.py и
раздельным allowlist-кодом в import_export.py, поля физически в разных
записях. Это наблюдение, а не риск для реализации.
Почему Low: не блокирует ни один AC и не создаёт технического противоречия
— два независимых пространства имён случайно тёзки. Стоит одной строкой
упомянуть в docs/CONFIG-COMPATIBILITY.md при обновлении (AC12 всё равно
трогает этот файл), чтобы будущий читатель не тратил время на тот же вопрос,
который потратил ревьюер.
Решение ревьюера: снимается с записью; не требует правки ТЗ или кода.
Что проверено и корректно
- Трек
fullобоснован по §5: новый UX-контракт (реверс модификатора вращения для мебели, новые ручки, отражение), задета сохраняемая модель/backend-схема, публичный контракт (знаковые размеры в свойствах) — минимум три из пяти критериев лёгкого трека нарушены одновременно, что и требуется для отказа отsmall. - Скоуп/не-скоуп разделены чётко и по кодовой границе: явно исключены rect/ellipse/text/backdrop, wall magnet, размещение, скос/деформация линий символа, touch UX. Это соответствует «Отдельный furniture-путь без изменения rect/ellipse/text/backdrop» и снижает риск регрессии на общем контроллере.
- Persisted-модель (§6) — обратимо-добавочная,
falseне материализуется, старый рендер/бэкенд не ломается — соответствует установленному в проекте паттерну optional-полей (CONFIG-COMPATIBILITY.md). - AC10 корректно требует отклонения не-boolean для
flip_h/flip_vи сохранения отказа backend на неположительныхw/h— это уже сегодняшнее поведение_FURN_SIZE, и ТЗ его не трогает, только добавляет два optional boolean рядом. - Область выбора (§7) осознанно отличается от готового паттерна
.derasehit/.dselecthit(экранные 16px,vector-effect: non-scaling-stroke): ТЗ требует физические 10 см, зависящие отcell_cm/zoom, и в «Рисках» отдельно прописан план на случай, если один stroked path не даёт корректный офсет при неравномерном масштабе символа. Это не оставлено на «как получится». - i18n-таблица (не считая Low-1) содержит все четыре словаря сразу, без «дозаполним EN/RU, а DE/FR потом».
- Откат описан предметно: что можно откатить (frontend UX/render), что нельзя без отдельной data-fix (снятие полей), и явно запрещён «слепой одновременный revert frontend+backend после публичного сохранения флагов» — типичное место, где специи обычно останавливаются на «миграции нет» и не договаривают.
Чего не проверял
- Не проверялся код — его не существует на этот SHA (задача ещё в
S3-spec/S4-spec-review, доS5-ready). Гейтыtypecheck/test/buildна этом этапе неприменимы: нечего собирать. - Не проверялась реализуемость «дискриминации доминирующей оси» (§1.4) на реальных furniture-символах с сильно неравномерным aspect ratio за пределами чтения формулы — это войдёт в код-ревью вместе с unit-таблицей.
- Не проверялся визуальный контраст новых средних ручек в тёмной теме — проверяемо только golden-эталоном, которого нет до реализации.
- Не проверялась точность SVG data-URI курсора вращения — AC8 сам называет способ доказательства «manual screenshot», то есть не автотест; это сознательное решение автора, не пробел ревью.
Гейты этого ревью
Ревью ТЗ гейтов сборки не требует (нет продуктового кода на этом SHA).
Прогонялся только git-осмотр репозитория (чтение файлов, git rev-parse HEAD); ни один build/test/typecheck не запускался и не нужен для этого этапа.
Унаследовано из r
Не применимо — это первый заход (r1), возвратов на правки не было.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
Обе находки — Low, обе решены ревьюером на месте (не блокируют, не требуют
повторного цикла). Issue может перейти в S5-ready.