27 KiB
SPEC-REVIEW-663-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/663
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: полный. Автор сам назвал критерий §5, который задача не проходит: «новая UX-сущность, новая persisted-модель, несколько поверхностей, межэтажные ссылки, touch и визуально-производительные риски» — это разбор по существу, полный трек выбран корректно.
- Материал: тело issue #663 (раздел «## ТЗ r1») + все 4 комментария:
(1) аналитика/оценка и SCOPE-сверка, (2) вопросы владельцу Q1–Q8 пачкой с
предложенными default, (3) решения владельца от 26.09.2026 по Q1–Q8, (4)
«ТЗ r1 готово к независимому ревью»,
blockedснят, переход вS4-spec-review. - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью
Новая сущность плана «Лестница»: прямоугольный марш с автогенерируемой
разметкой ступеней (шаг 30 см), стрелкой физического подъёма, transform/magnet
по контракту мебели/декора, односторонней ссылкой target_space_id на другое
House Plan space, кликом-переходом в View, вычитанием площади пересечения из
чистой площади помещений и плоской проекцией в 2.5D. На целевом этаже ничего
автоматически не создаётся (решение владельца Q1). Объёмный 2.5D, составные
лестницы, строительные расчёты и коллизии с проёмами/мебелью/устройствами явно
вне скоупа.
SCOPE-проверка (docs/SCOPE.md): задача закрывает J1 (физическая связь
этажей видна на плане), J4 (GUI-only, без Inkscape/YAML) и J6 (лестница
участвует в общем плане: undo/redo, optimistic conflict, import/export). Из
списка «никогда не строить» ничего не задевается: раздел 3 и «Не в скоупе»
явно и многократно отказываются от строительного расчёта (уклон, число
подступенков, нормы), отверстия/шахты в перекрытии и объёмной модели — то есть
задача не сползает в «General CAD» (docs/SCOPE.md, exception #53 остаётся не
затронутым: экспорт PDF здесь не расширяется). Новой scope-дыры не вижу.
Как проверялось
- Прочитаны
docs/SCOPE.md,AGENTS.md,docs/process/REVIEWER.mdцеликом; по ссылкам конспекта открыт PROCESS.md §2.4, §2.5, §4, §7.1, §7.2 (§2.10 не применялся — это r1). - Прочитано тело issue #663 целиком (
gh issue view 663 --json body) и все 4 комментария (gh issue view 663 --json comments) — история вопрос/ответ владельца воспроизведена в «Материал» выше. - Сверены обязательные разделы §7.1: Сценарий (1), Что человек увидит до/после (2), Проблема и продуктовая граница (3), Скоуп/Не-скоуп (4–5), Контракт поведения 6.1–6.5, UX свойств (7), Модель данных и совместимость (8), i18n (9), AC1–AC13 с указанным способом доказательства (10), План автотестов (11), Риски (12), Откат (13), Release-артефакты (14), «Принятые технические предположения» (15) — все на месте и в правильном порядке; первые два раздела продуктовые и без терминов реализации, как требует §7.1.
- Все 8 вопросов владельцу (Q1–Q8) сверены построчно с итоговым контрактом §6.1–6.3, §7: каждый ответ владельца от 26.09.2026 действительно внесён в текст (одностороннее размещение только на одном этаже — §6.3/решение Q1 совпадает с «Решение владельца» вводного абзаца; сохранённый zoom/позиция цели без автоцентрирования — §6.3/Q2; magnet по контракту декора с учётом толщины стены — §6.2/Q3; шаг 30 см, остаток сверху — §6.1/Q4; стрелка = физический подъём — §6.1/Q5; вычитание только площади, без других геометрических/световых эффектов — §6.4/Q6; fixed-floor виден, переход отключён — §6.3/Q7; 2D можно раньше 2.5D, первая версия 2.5D плоская — §6.5/Q8). Открытых продуктовых вопросов не осталось — подтверждаю.
- Контракт transform/magnet (§6.2, §8) и модель площади/навигации сверены с
каноном подсистемы:
docs/CANVAS.md§9.4 (Shift-семантика), §9.5 (Optimize/grid-pass),docs/WALL-THICKNESS.md(магнит мебели к «сырым» физическим телам стен),docs/UX-MODES.md(floor-закреплённая карточка, свайп/kiosk),docs/USER-GUIDE.ru.md(floor:/default_floor, «сохранённый вид просмотра» на пространство). Расхождение найдено и разобрано ниже. git branch -a/git log --all --oneline | grep 663— веткиissue/663-*и коммитов с трейлеромIssue: #663не существует;git diff origin/dev...HEADпуст. Продуктового кода для #663 нет — стадияspec, гейты (tsc,test,build, смоки, golden, инварианты) неприменимы, рассматриваю это как штатное состояние, а не находку.
Находки
High-1 — §6.2 (wall snap) и §8 (canonicalization/Optimize) описывают два
несовместимых контракта позиционирования, и ТЗ не говорит, какой из них
Что не так. ТЗ последовательно называет transform/magnet лестницы «по
существующему контракту мебели/декора» (разделы 1, 4, 6.2 — «Interaction-модель
повторяет мебель/декор»), как если бы «мебель» и «декор» были одним контрактом.
Канон docs/CANVAS.md §9.4 (строки 512–527) описывает их как два разных
контракта:
- у мебели (
furniture) — свой «wall magnet» (строка 523: «bypassing the furniture wall magnet»), координаты continuous/не квантуются («Furniture resize is the explicit exception to positional quantisation… Shift on its rotation handle snaps to 45°», строки 524–527), иdocs/WALL-THICKNESS.md:529–532явно называет это «furniture magnet semantics», привязанной к «сырым» физическим телам стен, а не к канонической (round-friendly) массе; - у обычного декора («ordinary decor») — Shift даёт свободный поворот, не
привязку к 45° (строка 522: «free ordinary-decor/backdrop rotation» — прямо
противоположно тому, что описывает мебель), никакого «wall magnet» нет
(строка 523–524: «the ordinary decor/room/grid magnet remains active»,
отдельно от мебельного), и координаты квантуются на грид (
docs/CANVAS.md:591–592: «Other decor kinds and storage-level numeric canonicalization keep their existing grid contract»). docs/CANVAS.md:588–591объясняет прямо, зачем это разделение существует: «The grid pass deliberately excludes the complete transform offurnitureand uploadedimagedecor. Their position, size and rotation are continuously authored values (#383), so changing even one of those fields would make Optimize create debt from a normal editor operation.»
ТЗ §6.2 описывает поведение, которое буквально совпадает с «мебельным»
wall magnet — «ставит её габарит вплотную к этой грани, а не к оси стены» —
то есть continuous-позиция, зависящая от произвольной толщины стены (толщина
задаётся в сантиметрах непрерывно, 0–100 см, по умолчанию 15 см —
docs/WALL-THICKNESS.md:383–388; половина толщины в общем случае не кратна
шагу грида, который определяется физическим масштабом конкретного плана,
docs/CANVAS.md:97–120). Одновременно §8 требует: «canonicalization округляет
координаты тем же lattice-контрактом, что decor» (issue #663, тело, строка
126) — то есть буквально контракт обычного декора, тот самый, который
включён в грид-квантование на каждом сохранении. Тут же в том же перечне §8:
«Optimize не изменяет лестницы» (строка 127) — то есть явный «мебельный»
контракт исключения из Optimize.
Оба требования из §8 не могут выполняться одновременно с §6.2, если читать «lattice-контракт decor» буквально: если координаты лестницы округляются на гриде при каждом сохранении (как у обычного декора), то габарит, магнитно примкнутый к грани стены на дробном расстоянии (толщина/2), будет сдвинут на ближайший узел грида уже на первом же «Сохраняет план» (сценарий п.5) — магнит перестаёт держать, ровно та проблема, ради которой канон исключил мебель из грид-прохода. Если же имелось в виду «контракт мебели» (continuous, исключён из Optimize) — тогда фраза «тем же lattice-контрактом, что decor» написана неточно и вводит в заблуждение, поскольку канон использует слово «decor» именно для контраста с «furniture».
Почему это находка ревью ТЗ, а не техническая деталь на усмотрение автора. Это не «где хранится состояние» или «какой файл», а прямое противоречие контракту поведения, наблюдаемому пользователем: либо лестница держит магнит к стене после сохранения (мебельный контракт), либо не держит и молча съезжает (декор-контракт) — то есть ровно то расхождение, которое ломает сценарий п.3 («магнитится вплотную к видимой грани стены») и AC2. Без исправления реализация с равной вероятностью выберет любой вариант, оба пройдут AC2 как написан («примыкает… не меняется» ничего не говорит про persist/Optimize), и один из двух вариантов — задокументированный баг. Раздел 15 «принятые технические предположения» это расхождение не упоминает, хотя оно влияет на пользовательский контракт («держит ли лестница магнит после сохранения») — а значит, по правилу §7.1, не может быть закрыто автором «свободно», не задев наблюдаемое поведение.
Что нужно. Заменить оба места одним однозначным правилом: либо (а)
лестница использует continuous-позиционирование, как мебель — исключена из
alignAllToGrid/Optimize-грид-прохода и из посейвовой численной канонизации,
магнит к стене/другой лестнице сохраняется как есть, — либо (б) лестница
квантуется на гриде как обычный декор, и тогда §6.2 должен явно сказать, что
результат wall snap дополнительно проецируется/округляется на грид (со
следствием — либо гарантией, что это округление не разрушает «вплотную»
визуально в пределах допуска, либо явным отказом от точного «вплотную»). Любой
из двух вариантов реализуем; невыполненным ТЗ является именно отсутствие
выбора между ними, поскольку оба явно записанных требования (§6.2 и §8)
взаимно исключают друг друга при буквальном прочтении.
Серьёзность: High — блокирует. Затрагивает AC2 (magnet), AC9 (нет побочных эффектов при Optimize) и AC11 (round-trip после сохранения).
Low-1 (снят без возврата) — AC7 называет четыре состояния сломанной
ссылки, контракт определяет три
§6.3 перечисляет ровно три состояния: «цель отсутствует, равна текущему этажу или была удалена». AC7 (тело issue, §10) добавляет четвёртое слово: «пустая, self, удалённая и недоступная». Нигде в тексте не определено, чем «недоступная» отличается от «удалённая» (или это синоним, дублирующий формулировку §7 «предупреждение о недоступной цели», которая относится ко всем трём состояниям сразу). Практического расхождения не создаёт — тестируемые состояния однозначно определены в §6.3, а AC7 в момент реализации будет проверяться по ним, — но словарь стоит привести к одному термину, чтобы четвёртое слово не читалось как отдельный незадокументированный кейс (например, существующий, но временно недоступный space — такого понятия в модели нет). Снимаю как Low, без возврата автору на цикл; поправить при следующей правке ТЗ заодно с High-1.
Low-2 (снят без возврата) — AC13 не называет конкретный бюджет
«…большой fixture остаётся в действующих бюджетах» не указывает, какой именно
бюджет/метрику код-ревью будет сверять (frame time? per-space render budget?
bundle:budget-подобный числовой порог?). Способ доказательства
(performance + ревью кода) назван, что формально закрывает требование DoR
«влияние на производительность названо», но конкретное число решится только на
код-ревью. Это приемлемо для технической детали (сама формулировка «кэшируемая
геометрия, не пересчитывать на каждый live-state update» уже задаёт
проверяемый принцип), но стоит явно сослаться на конкретный существующий
performance-смок/бюджет при реализации, чтобы код-ревью не пришлось изобретать
критерий с нуля. Снимаю как Low.
Что проверено и корректно
- Все обязательные разделы §7.1 присутствуют, в правильном порядке; раздел 1 называет персону (администратор), поверхность (редактор плана) и момент; раздел 2 — одной фразой, без терминов реализации.
- Скоуп/Не-скоуп разделены явно и совпадают с §3 (навигационная, не расчётная сущность) и с ответами владельца Q1–Q8 — расхождений между вопросами, default-предложениями и итоговым текстом не найдено (см. «Как проверялось» п.4).
- Контракт стрелки/ступеней (6.1) внутренне согласован и проверяем: полный шаг 30 см от нижней границы, неполный остаток только у верхней — не оставляет места для двух прочтений, AC3 численно завершает контракт.
- Контракт навигации (6.3) корректно совпадает с реальным поведением
закреплённой карточки
floor:(docs/USER-GUIDE.ru.md:2266–2271: «Закреплённая карточка… игнорирует… вкладки других пространств») и с «сохранённым видом просмотра» на пространство (docs/USER-GUIDE.ru.md:488) — AC5/AC8 проверяемы по существующим механизмам, не по гипотетическим. - Контракт площади (6.4) однозначно ограничивает эффект только числом («это расчёт площади, а не отверстие в рендере»); AC4/AC9 закрывают его без зазора для «а что с полом/светом/vacuum» — отдельно и явно исключено.
- Модель данных (8) — опциональная коллекция,
stairsотсутствует ⇒ старое поведение сохраняется — соответствует установленному в проекте паттерну (аналогичноsettings.volumetric_view/иным опциональным bounded-коллекциям); ремонт битой ссылки «по стабильнымspace.id, никогда по порядку или названию» закрывает главный риск раздела «Риски». - Каждый AC1–AC13 указывает способ доказательства (
unit/smoke/golden/backend/geometry parity/performance+«ревью кода»), что удовлетворяет требованию DoR §2.5. - Продуктовые вопросы Q1–Q8 заданы владельцу одним комментарием, пачкой,
каждый с предложенным default (соответствует форме §7.1); ответы получены,
внесены в тело,
blockedснят — открытых продуктовых вопросов не осталось. - i18n-таблица (9) перечисляет ключи по смыслу (инструмент/сущность, длина, ширина, поворот, направление, «ведёт на этаж», недоступная цель, fixed-floor no-op) и явно запрещает конкатенацию строк.
- Риски (12) называют главные технические угрозы (SVG-нагрузка на малом zoom, расхождение площади между потребителями, ложные touch-переходы, битые ссылки, смешение с decor-транформ-хелперами, объёмный будущий этап) — ровно те, что также разобраны в «Аналитике» комментария 1; совпадение полное, ничего не потеряно между аналитикой и ТЗ.
- Откат (13) корректно описывает опциональность коллекции и безопасность отключения без разрушения пользовательских данных — соответствует правилу SCOPE.md «никогда не удалять файл/данные пользователя на инференс».
- Release-артефакты (14) называют оба changelog, документацию (User Guide, UX-MODES, architecture/config-compatibility, 2.5D), screenshots и пользовательскую заметку о плоской первой версии — полный список, ничего не забыто относительно затронутых канонических документов (п.6 «Читай в этом порядке»).
Чего не проверял
- Гейты (
tsc --noEmit,npm test,npm run build,check-docs.mjs, смоки,golden:verify, инварианты) — не прогонял: этапspec, ветки/коммитов для #663 нет,git diff origin/dev...HEADпуст. Предмет код-ревью после реализации. - Не пересчитывал буквально шаг грида на конкретных числовых примерах (взял
общее свойство «половина произвольной толщины стены в общем случае не кратна
шагу грида, зависящему от масштаба плана» из §3/§9.4-9.5
docs/CANVAS.mdи диапазона толщиныdocs/WALL-THICKNESS.md:383-388); для High-1 это не требуется — контракт противоречив независимо от конкретных чисел, при любом шаге грида, кроме случая, когда автор случайно выберет толщину стены, кратную шагу (не гарантировано моделью). - Не проверял осуществимость 2.5D-этапа сверх текста ТЗ (нет макетов дизайнера, раздел 15 и Q8 явно относят конкретную визуальную реализацию к «принято предположительно»/будущим материалам) — не предмет ревью ТЗ.
- Не оспаривал сами решения владельца Q1–Q8 — только сверял, что итоговый контракт им соответствует (см. «Как проверялось» п.4); продуктовая правомерность выбора («одностороннее размещение», «шаг 30 см» и т.д.) — решение владельца, не предмет спора ревьюера.
- Не проверял i18n-ключи построчно на предмет конкретных строк RU/EN — раздел 9 перечисляет смысловые группы ключей, конкретные строки — реализация.
Вердикт
Обязательные разделы ТЗ полны, 8 продуктовых вопросов закрыты владельцем без
остатка, 12 из 13 AC однозначны и не создают внутреннего противоречия. Но
контракт transform/magnet (§6.2) и контракт canonicalization/Optimize (§8)
описывают лестницу одновременно как «мебель» (continuous, wall magnet,
исключена из Optimize) и как «обычный декор» (grid-bound, включена в
posейвовую канонизацию) — два взаимоисключающих поведения канона
docs/CANVAS.md, между которыми ТЗ не выбирает. Это не техническая деталь,
свободная для реализации: неверный выбор — реальный, воспроизводимый на первом
же сохранении баг («лестница съезжает со стены»), который AC2 в текущей
формулировке не ловит. Возврат автору для явного выбора одного из двух
контрактов (и правки §6.2 либо §8 под него).
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 1 · Medium: 0 → в задаче
Материал раунда
- Ветка:
dev, коммит422221fe9342— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
c4e3f48a513e0468285959a11f937aa9b13c9754git log --all --format='%H %T' | grep c4e3f48a513e - Тело issue:
a304102f2b7c3f5aae7814eabe642e7aa679d5243df7f25ce579bc490959262c - Вердикт конвейера:
yellow· High 1