Files
houseplan-card/docs/reviews/SPEC-REVIEW-445-r1.md
T
2026-09-03 21:57:57 +00:00

252 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-445-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/445
- Этап: spec (PROCESS.md §2.4), трек: full (обоснован автором в S2-analysis: сложность/риск выше 3, контракт проходит через несколько модулей — критерий `small` не выполнен)
- Материал: `docs/specs/445-furniture-wall-face-snap.md`, коммит `f6879b37` на ветке `issue/445-furniture-wall-face`, дерево полностью включает `origin/dev` (merge-base = HEAD dev, `560ca214`) — ребейз не требуется
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (лимит на полном треке — 4)
## Скоуп разбора
Первый заход — разбор полный. Материал: тело issue #445, все 4 комментария
(аналитика → вопросы владельцу → решение владельца → «ТЗ готово»), сам файл ТЗ,
код, на который ТЗ ссылается (`_furnWalls`, `snapFurnitureToWall`,
`_rawPhysicalBodiesR`, `_segments`/`roomEdges`, `_innerRoomContour`,
`FURN_WALL_CELLS`, `test/furniture.test.mjs`), `docs/SCOPE.md`,
`docs/WALL-THICKNESS.md`, `docs/FURNITURE.md`, `docs/TOUCH-SUPPORT.md`,
`docs/USER-GUIDE.ru.md` (терминология магнита/мебели).
## Как проверялось
1. Прочитан diff коммита `f6879b37` (`git show --stat`): только
`docs/specs/445-furniture-wall-face-snap.md` (новый, 377 строк) и одна
строка в `docs/specs/README.md` (реестр). Класс C, `src/**`/`test/**` не
затронуты.
2. Сверены техническая часть ТЗ («Подтверждённая проблема», карта реализации)
с текущим кодом на этом же SHA:
- `_furnWalls` (`src/houseplan-editor-runtime.ts:4907-4912`) действительно
возвращает `[...this.host._segments, ...faces]`, где `_segments`
(`src/houseplan-card.ts:7385`) — осевые `roomEdges`, а `faces` — грани
только `_rawPhysicalBodiesR()` (drafts/partitions/columns). Подтверждено.
- `snapFurnitureToWall` (`src/furniture.ts:492` и далее) кладёт BACK на
переданный сырой `[x1,y1,x2,y2]` и при `nl < 1e-9` берёт левую нормаль
ребра (`nx = dy/len; ny = -dx/len`) — независимо от того, где комната.
Подтверждено, включая номера строк и комментарий «dead on the wall».
- `test/furniture.test.mjs:171-177` строит `EDGES` через `roomEdges([ROOM])`
без какого-либо каталога стен и ожидает `s.cy = 100 + 45` (осевая + треть
глубины) — тест действительно проверяет центролинейное поведение и не
видит толщины. Подтверждено.
- `docs/WALL-THICKNESS.md` §9 заканчивается фразой «Raw bodies remain
authoritative for hit testing, selection, drag, properties, deletion,
history and furniture magnet semantics» — независимые тела уже физические
грани, повторный offset для partitions/columns действительно был бы
ошибкой; ТЗ учитывает это явно (раздел «Физический кандидат стены»,
последний абзац).
Технических неточностей или недоказанных утверждений о текущем поведении
не найдено — «Подтверждённая проблема» и карта реализации описывают код,
который есть на диске, а не воображаемый.
3. Сверены defaults владельца (Q1/Q2, комментарий «Решение владельца») с
разделами «Контракт поведения» и «Принятые предположения» ТЗ — построчное
соответствие есть, ни один owner default не потерян и не расширен без
основания.
4. Сверена терминология с `docs/USER-GUIDE.ru.md` (строки 1306, 1390-1391):
«магнитится к стене», «прижимается к ближайшей стене в радиусе магнита» —
ТЗ не вводит новых пользовательских терминов; текущее руководство не
противоречит новому поведению (обещание «ближайшая стена» остаётся
истинным при переходе с осевой линии на поверхность).
5. Проверено соответствие `docs/SCOPE.md`: задача — уточнение геометрии уже
существующей функции редактора (мебель, wall magnet), в скоупе J4/J6
(«два встроенных редактора», «keep the plan true»); никакой новой сущности
или инструмента не вводится, collision model явно исключена в не-скоуп.
Конфликта со SCOPE нет.
6. Проверена структура разделов на соответствие §7.1 и фактическому шаблону
репозитория — сверено с недавним принятым full-track ТЗ
`docs/specs/442-marker-write-rollback.md` (тот же набор заголовков
один-в-один, включая свёрнутые «UX»/«i18n» в «Touch, клавиатура и
доступность» + «Данные и совместимость»). Отклонения от установленного в
проекте формата нет.
7. Прошёлся по каждому пункту «Контракт поведения» и таблице «Ошибки и
крайние случаи» и сопоставил с AC1…AC9 построчно (см. находку ниже).
8. Гейты (typecheck/test/build) **не прогонялись** — см. «Чего не проверял».
## Находки
### Medium (в скоупе задачи) — угловой tie-break не имеет доказательства
**Файл:** `docs/specs/445-furniture-wall-face-snap.md`, раздел «Контракт
поведения» → «2. Выбор стороны», пункт 6, и таблица «Ошибки и крайние
случаи», строка «угол с двумя равноудалёнными стенами».
**Формулировка контракта:** «В углу либо при равном расстоянии до нескольких
стен выигрывает физически ближайшая поверхность, затем сторона намерения,
затем стабильный tie-break. Перестановка входных массивов не меняет
результат.» Это новое, видимое пользователю поведение: раньше `_furnWalls`
отдавал единственный плоский список сегментов и ближайший побеждал по
`bestD` без всякого различения «своя стена / другая стена»; теперь список
кандидатов — многослойный (несколько candidate на одну стену + грани
независимых тел), и на прямом углу между двумя РАЗНЫМИ стенами при точном
равенстве расстояний нужен новый, ранее не существовавший tie-break.
**Проблема:** ни один из AC1…AC9 не называет эту ветку. Сверка построчно:
| Строка контракта / таблицы | AC, который её доказывает |
|---|---|
| Внешняя стена, pointer внутри/на оси/снаружи | AC1 |
| Общая стена, обе стороны, exact-axis drag/placement, permutation/winding | AC2 |
| Локальная атомарная толщина на переходе 10→20 см | AC3 |
| Радиус от поверхности | AC4 |
| Preview/commit/move один контракт | AC5 |
| Нулевые стены, независимые тела, malformed segment | AC6 |
| Touch safety, схема данных | AC7 |
| Визуальный результат | AC8 |
| Кэш/производительность | AC9 |
| **Угол с двумя равноудалёнными РАЗНЫМИ стенами, порядок массива** | **нет** |
AC2 доказывает перестановочную инвариантность только для ОДНОЙ общей стены
(смена ролей «комната A / комната B» на одном и том же физическом сегменте),
а не выбор между двумя разными, обычно неколлинеарными стенами, сходящимися
в углу. «План тестирования» упоминает «permutation/winding» одним пунктом без
номера AC, и по контексту (соседний список «внешней/общей стены, intent
point, exact-axis tie») это тоже про AC1/AC2, а не про многостеновой случай.
**Почему это находка, а не придирка:** сам автор счёл сценарий достаточно
реальным, чтобы вписать его отдельной строкой в таблицу крайних случаев —
то есть это не гипотетический край, а заявленный контракт. Раз для него не
названо ни `unit`, ни `smoke`, ни какого-либо иного способа доказательства,
код-ревью не сможет ответить на вопрос «оно вообще работает» для этой ветки:
по правилу §2.7 «оно либо доказано автотестом, либо разобрано по коду с явной
записью „проверено чтением“» — а без AC ревьюер кода не обязан вообще на неё
смотреть, потому что она не пронумерована. Здесь же для смежных строк того же
уровня риска (§2.6 — общая стена + exact-axis, тоже про несколько кандидатов)
доказательство есть, а для строки про несколько РАЗНЫХ стен в углу — нет.
Отсутствие пронумерованного AC для заявленного поведения — прямое нарушение
DoR §2.5 («AC1…ACn — пронумерованные проверяемые критерии приёмки»), а
не эстетическое пожелание.
**Чем закрывается (в этой же задаче, без нового issue):** добавить AC10 (или
расширить AC2) — unit-фикстура с двумя неколлинеарными стенами на равном
расстоянии от точки намерения (например, прямой угол комнаты), проверяющая,
что: 1) побеждает физически ближайшая поверхность при неравном расстоянии;
2) при точном равенстве расстояний результат детерминирован и не зависит от
порядка `walls`/`_rawPhysicalBodiesR()` на входе; 3) сторона намерения
учитывается как второй критерий раньше стабильного tie-break. Метод
доказательства — `unit`, по аналогии с остальными AC этого ТЗ.
Это Medium в скоупе задачи (сам контракт уже описывает поведение, требуется
лишь пронумеровать доказательство) — без High-находок это жёлтый вердикт,
правка ТЗ и повторный цикл в рамках того же issue, отдельный issue не
заводится (#202).
## Что проверено и корректно
- Обязательные разделы §7.1 присутствуют и в порядке, совпадающем с недавно
принятым полным ТЗ (#442): сценарий → что человек увидит → проблема →
скоуп/не-скоуп → контракт → данные/совместимость → touch → крайние случаи →
AC → план тестов → карта реализации → риски/rollback → release-артефакты →
принятые предположения.
- «Сценарий» и «Что человек увидит до и после» отвечают на оба продуктовых
вопроса без терминов реализации; персона (Home admin, Редактор подложки)
названа явно.
- Все технические утверждения о текущем поведении («Подтверждённая проблема»)
проверены построчно по коду на SHA `f6879b37` и совпадают: `_furnWalls`,
`snapFurnitureToWall`, `nl < 1e-9` ветка, состав `test/furniture.test.mjs`.
Ни одной догадки, выданной за факт: там, где ТЗ вводит новое поведение
(радиус от поверхности, сторона по намерению, tie-break), это либо прямое
происхождение от ответа владельца (Q1/Q2), либо явно помечено в разделе
«Принятые предположения» и открыто для оспаривания.
- Открытые продуктовые вопросы (Q1, Q2) заданы владельцу по правилам §7.1:
пачкой, с предложенным default, `blocked` был выставлен, ответ получен и
корректно перенесён в контракт без искажений — сверено построчно с
комментарием «Решение владельца».
- AC1…AC9 (за вычетом находки выше) однозначны, у каждого назван способ
доказательства (`unit`/`browser smoke`/`golden`/`code review`) и, где AC
заявляет защитное свойство (радиус, отсутствие двойного offset, fallback
на левую нормаль), назван вид mutation, которая обязана его покраснить —
это опережает требование §2.7 и облегчает будущее код-ревью.
- Скоуп/не-скоуп корректно исключает collision model, resize/rotate/flip
(#383), изменение SVG каталога и толщины/рендера самих стен — ни один из
этих пунктов не просачивается в контракт ниже.
- Данные и совместимость: persisted schema не меняется, миграции нет, i18n
ключи не добавляются — согласовано с `CONFIG-COMPATIBILITY.md` духом
(никакой новой версии конфига не описано и не требуется).
- Touch/доступность: safety floor описан терминами, совпадающими с
`docs/TOUCH-SUPPORT.md` («clean tap», «pinch/second pointer/pointercancel
ничего не сохраняют»), а не изобретён заново.
- Ссылка issue ↔ ТЗ двусторонняя: тело ТЗ ссылается на issue, `docs/specs/
README.md` получил строку на #445 в том же коммите.
- Трейлеры коммита `f6879b37`: `Issue: #445`, `User-Visible: no` — корректно
для документации, класс C.
- SCOPE.md: задача — точечное исправление геометрии существующей функции
(мебель/wall magnet) внутри уже описанных J4/J6, конфликта с гранью скоупа
нет; в out-of-scope списке SCOPE.md ничего из контракта не задето
(не 3D, не collision model интерьера, не automations).
## Чего не проверял
- **Гейты `typecheck`/`test`/`build`/`bundle:sync` не прогонялись.** Диапазон
этого коммита (`git show --stat f6879b37`) касается только
`docs/specs/445-furniture-wall-face-snap.md` и одной строки в
`docs/specs/README.md` — класс C, ни одного файла `src/**`/`test/**`/
`scripts/**`. На этапе ТЗ (spec review, PROCESS.md §2.4) предмет разбора —
выполнимость и проверяемость ТЗ, а не состояние продуктового кода, которого
в этом коммите нет; прогон typecheck/test/build на docs-only дельте не дал
бы никакого сигнала об этой дельте (это баланс тестового бюджета этого же
документа: «полные наборы — предрелизный гейт, а не гейт ревью», и здесь
гейты просто не относятся к изменённым файлам). Они обязательны на
код-ревью, когда появится реализация.
- **`node scripts/check-docs.mjs` не прогонялся** — diff не касается `src/**`,
условие запуска не выполнено.
- **`node scripts/model-invariants.mjs` не прогонялся** — diff не меняет
геометрию, `layout`, `wall_segments` или ссылки на них; это план на будущее
изменение продуктового кода, а не на сам текст ТЗ.
- **Browser smokes / golden / performance / backend pytest не прогонялись** —
нет реализации для запуска; это часть плана тестирования будущего
код-ревью, а не спецификации.
- **Не проверял вручную UI/демо-стенд** — на этапе spec review нет ни кода,
ни поведения для наблюдения; сценарии таблицы «Ошибки и крайние случаи»
проверены логическим разбором формул (offset от `halfDepth`, distance-to-
surface) против описанного в `docs/WALL-THICKNESS.md` контракта атомарной
толщины, не исполнением.
- Не проверял, называет ли будущая реализация конкретный алгоритм
«стабильного tie-break» для нового размещения на оси общей стены (raздел
2, пункт 4) — ТЗ сознательно оставляет выбор конкретной комнаты автору
(«принято предположительно, поменять свободно» не помечено буквально, но
сам владелец в defaults сформулировал это как «детерминированная сторона
ОДНОЙ ИЗ смежных комнат», то есть какая именно — уже не продуктовый вопрос).
Это отличается от находки выше тем, что там отсутствует не алгоритм, а
само доказательство/AC.
## Материал раунда
- Ветка: `issue/445-furniture-wall-face`
- SHA материала: `f6879b3788fc6ddc2626482ce0a890edb6b0200b`
- Дерево материала: `docs/specs/445-furniture-wall-face-snap.md` (blob на
этом SHA), `docs/specs/README.md`
- merge-base с `origin/dev`: `560ca214b0d7d9018ff7916b25ebaea96a80e912`
(= HEAD `origin/dev` на момент ревью — дельта пуста, ребейз не требовался)
## Вердикт
Одна находка Medium в скоупе задачи, High нет. Согласно §2.4/§2.7 без
High-находок это жёлтый вердикт: автор добавляет AC (или расширяет AC2) для
углового tie-break с явным способом доказательства и перевыставляет
`S4-spec-review` — правка укладывается в один абзац контракта и не требует
пересмотра остальной части ТЗ.
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/445-furniture-wall-face`, коммит `f6879b3788fc` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `c3ea0c69a5b4fa897e29ec294994f771189b9a1d`
```
git log --all --format='%H %T' | grep c3ea0c69a5b4
```
- ТЗ `docs/specs/445-furniture-wall-face-snap.md`, блоб `6c191fb7e9e65afd6e85f5fd0d80576ce4d2e7d3`
```
git log --all --find-object=6c191fb7e9e65afd6e85f5fd0d80576ce4d2e7d3 -- docs/specs/445-furniture-wall-face-snap.md
```