Files
houseplan-card/docs/reviews/SPEC-REVIEW-253-r1.md
T
2026-08-23 08:30:43 +00:00

172 lines
17 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-253-r1
- Issue: [#253](https://github.com/Matysh/houseplan-card/issues/253) — «Ресайз в некоторых случаях теряет толщину стен: перемещённые рёбра остаются осевыми линиями»
- Этап: ТЗ на ревью (PROCESS.md §2.4)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Артефакт под ревью: `docs/specs/253-resize-wall-thickness.md` (+ строка в `docs/specs/README.md`)
- Ветка: `issue/253-resize-wall-thickness`
- SHA ревью: `d9f7861` (`git diff origin/dev..d9f7861` — единственная дельта, новый файл ТЗ + запись в реестре)
- Вердикт: **зелёный**
## Скоуп ревью
Первый заход (r1), раздела «Унаследовано из r0» и «Закрытие раунда r0» не требуется —
предыдущего вердикта не существует. Разбор — полный документ ТЗ по PROCESS.md §7.1
и §2.4, плюс верификация технических утверждений автора по фактическому коду `dev`
(а не по слову автора), поскольку ТЗ построено на числовом разборе конкретного
дефекта, и любая неточность в диагнозе сделала бы контракт нереализуемым.
## Как проверялось
1. Прочитано тело issue #253 и все 5 комментариев (owner) — включая исходный
отчёт, воспроизведение на приложенном экспорте (24→23 записи, потеря 33 см) и
финальную аналитику с «Требуемым поведением» и «Принятыми предположениями».
2. Прочитан `docs/SCOPE.md` (job J6), `AGENTS.md`, `PROCESS.md` §2.4/§2.10/§7.1,
`docs/WALL-THICKNESS.md`, `docs/USER-GUIDE.ru.md` (строка 456: контракт «интервалы
толщины и виртуальности переносятся вместе со стеной» уже документирован —
фикс восстанавливает существующее обещание, а не придумывает новое).
3. Полностью прочитан `docs/specs/253-resize-wall-thickness.md` (401 строка,
разделы §1–17) и сверен с обязательным списком PROCESS.md §7.1 — все разделы
на месте (сценарий, что человек увидит до/после, проблема, скоуп/не-скоуп,
контракт поведения, UX/touch, модель данных/миграция, i18n, AC1…AC10 с
доказательством, план автотестов, риски, производительность/безопасность,
откат, release-артефакты, принятые технические предположения).
4. Прочитан текущий код на `dev`, чтобы убедиться, что диагноз ТЗ (§3) не
догадка, а факт:
- `src/wall-thickness.ts:438` `rekeyWallsAfterMove()` — подтверждено: запись с
точными `a/b` переносится только когда **оба** конца лежат в допуске **одного**
старого ребра (`distToSeg(...) > tol → continue`), без разбиения на частичном
пересечении; коллизия `used.has(nk)` действительно тихо выбрасывает запись
(строка 512) без сравнения геометрии/толщины. Ровно то, что описывает §3 ТЗ.
- Проверена смежная гипотеза, которую ТЗ **не рассматривает явно**:
`_rszApplyPreview()` (`src/houseplan-card.ts:8323`) пропускает `oldSpans/newSpans`
комнаты целиком при `oldR.poly.length !== newPoly.length`, а `shiftSharedSpans()`
(`src/resize.ts:124`) прямо документирован как «the neighbour may become
L-shaped» — то есть вставляет вершину соседу при частичном перекрытии, меняя
число вершин. Прослежено до конца: `sharedSpansWith`/`shiftSharedSpans` клипуют
перекрытие соседа диапазоном `[0, L]` длины **основного** перетаскиваемого
ребра, поэтому перемещаемый интервал стены всегда целиком покрыт собственным
ребром «своей» комнаты (`plan.roomId`, чей `poly.length` при edge-drag не
меняется — `movePolyEdge` двигает только 2 вершины) и при corner-scale
(`applyRoomScale`: `room.poly.map(scalePt)`, вершины только линейно
преобразуются, число тоже не меняется). Пропуск соседа с изменившимся
`poly.length` избыточен, но не создаёт потери покрытия — не дефект.
Не в отчёте как находка, потому что после трассировки подтверждено отсутствие
эффекта; фиксирую здесь, чтобы дельта следующего раунда не переоткрывала тот же
путь.
- `src/coordinate-canonicalization.ts` — подтверждено существование
nine-decimal canonicalization (`COORDINATE_DECIMALS = 9`) и что `space.walls[].a/b`
уже входят в её allow-list (строки 95–98). Ссылка ТЗ (§6.4) на этот механизм не
фантазия.
- `scripts/model-invariants.mjs:163` `checkWallRecordsPreserved()` — подтверждено:
существующая инфраструктура #254 сравнивает мультимножество значений `cm` и
именно её комментарий цитирует дефект #253 как мотивирующий пример. Ссылка
ТЗ (§12.3) точна.
- `scripts/mutation-gate.mjs` — подтверждено: реестр мутантов, куда автор обязан
добавить новый мутант под #253; механизм существует и работает, как описано.
- `custom_components/houseplan/validation.py:802` `WALL_SCHEMA` /
`MAX_WALLS = 500` — подтверждено: схема не требует уникальности `key`, значит
"разные записи с одинаковым key" (контракт §6.4 ТЗ) не будет отвергнуто
backend-валидацией; заявление АС9 «persisted schema/backend неизменны»
корректно.
5. Самостоятельно прогнаны заявленные в хендоффе гейты ТЗ (не поверил на слово):
- `node scripts/check-docs.mjs --external` → `Documentation checks passed (7 files, 10 external links).`
- `node scripts/process-gate.mjs --range origin/dev..HEAD --issues` → `гейт пройден, предупреждений 0`
- `git diff --check origin/dev..HEAD` → чисто (exit 0)
- `docs/specs/README.md` — строка `#253` добавлена, ссылка на файл корректна.
## Находки
Нет находок уровня High или Medium (ни в скоупе, ни вне скоупа).
### Low (снимается ревьюером, с записью)
**L1 — риск `MAX_WALLS=500` от роста числа записей не упомянут в разделе рисков.**
Новый контракт (§6.2) может **увеличивать** число записей `walls[]` на ресайзе
(разбиение частично пересекающегося интервала на покрытый и непокрытый фрагмент) —
раньше число записей могло только оставаться неизменным или уменьшаться (склейка).
Таблица рисков (§14) не называет верхнюю границу `MAX_WALLS = 500`
(`custom_components/houseplan/validation.py:483`) и не описывает поведение при её
достижении (что произойдёт с записью — тем самым интервалом, который фикс обязан
не терять, если backend отвергнет весь `walls[]` целиком по `vol.Length(max=500)`).
Оставляю как Low: практический предел варианта не близок за один жест (нужны сотни
уже существующих записей ещё до правки), поэтому не блокирую — но при реализации
стоит явно решить (тест или защитный код), что происходит, если разбиение подведёт
план к границе. Снимается без правки ТЗ: авто-тесты плана (§12.1) и так покрывают
табличные случаи; граница `MAX_WALLS` не является частью контракта этой задачи и не
меняется ею — упоминание было бы уместно, но его отсутствие не делает ни один AC
непроверяемым.
## Что проверено и корректно
- **Продуктовая рамка.** §1 явно привязывает задачу к J6 `docs/SCOPE.md`
(«Keep the plan true as the home evolves»), персона/поверхность/момент указаны
конкретно (админ дома, desktop Plan editor, Resize верхней стены сауны).
«Что человек увидит» (§2) — одной парой фраз, без терминов реализации.
- **Диагноз — не догадка.** §3 ТЗ воспроизводит числовой разбор владельца
(24→23 записи, потеря записи 33 см) и код-трассировку `rekeyWallsAfterMove`;
оба независимо подтверждены чтением `src/wall-thickness.ts` (см. «Как
проверялось» выше). Ни одно поведенческое утверждение о текущей системе не
висит без опоры на код или на комментарий владельца.
- **Скоуп/не-скоуп** (§5) — явный и непротиворечивый: `open_spans`/UX/handles/
инструмент «Толщина»/миграция/Optimize/схема исключены с конкретной причиной
каждый раз, ничего не тянется «раз уж мы здесь».
- **Контракт интервалов** (§6) специфицирован как детерминированный алгоритм
(партиционирование по границам пересечений → перенос покрытых фрагментов по
линейному `t` → сохранение непокрытых): в нём нет шага, оставленного на догадку
реализатора. Коллизия `key` явно не может удалять запись (§6.4), что закрывает
корневую причину из §3 пункт 2.
- **AC1…AC10** (§11) — каждый с однозначным способом доказательства (unit-таблица,
production-bundle smoke на минимизированной fixture, model-invariant gate,
compatibility/docs-тесты); AC1 сформулирован числами реального сценария (24
записи, конкретные `x/y`), а не общими словами.
- **Модель данных/миграция** (§9) — схема не меняется, legacy key-only записи не
переписываются фоном, потеря уже случившаяся в старых версиях не восстанавливается
«из воздуха» — соответствует стоящему правилу `docs/SCOPE.md` не чинить то, для
чего нет исходных данных.
- **UX/touch** (§8) — новых контролов нет, desktop-first Resize остаётся таким,
общий safety floor (Esc/pointercancel не пишут) переносится без изменений;
согласуется с `TOUCH-SUPPORT.md`/`UX-MODES.md`.
- **i18n/документация/changelog** (§10, §16) — новых строк интерфейса нет; RU/EN
changelog и оба канонических документа (`WALL-THICKNESS.md`, `ARCHITECTURE.md`)
назначены к обновлению в том же коммите, как требует правило «документация в том
же коммите, что поведение».
- **Откат** (§15) — один revert продуктового коммита, без миграции данных; старая
версия читает результат как обычные atomic-записи (согласуется с моделью
`docs/WALL-THICKNESS.md`).
- **Принятые технические предположения** (§17) — все 6 пунктов действительно
невидимы пользователю или являются защитной веткой «не должно происходить при
валидных операциях» с собственной красной unit-диагностикой (§6.3); ни один не
маскирует продуктовый вопрос под техническое решение.
- **Гейты ТЗ.** `check-docs.mjs --external`, `process-gate.mjs --issues`,
`git diff --check` — все три перепрогнаны мной на SHA `d9f7861` и зелёные (см.
«Как проверялось»); запись в `docs/specs/README.md` соответствует требованию
PROCESS.md §7.3.
## Чего не проверял
- Не проверялась реализация — она не существует, задача в `S4-spec-review`, код
не тронут ни строкой (только `docs/specs/**` и `docs/specs/README.md`, класс C).
- Не запускал `npm run typecheck`/`npm test`/`npm run build` — на этой дельте
(только документация) они не относятся к предмету ревью ТЗ; дешёвые гейты кода
будут обязательны на этапе код-ревью.
- Не проверял `demo/smoke_resize_wall_thickness.mjs`, `test/wall-thickness.test.mjs`
расширения, новый мутант в `scripts/mutation-gate.mjs` — их не существует до
реализации; план для них (§12) оценён на полноту и однозначность, не на
исполнение.
- Не оценивал производительность/`O(W × E log E)` эмпирически — оценка сложности в
§13 принята как разумная асимптотика для входных размеров реального проекта
(десятки-сотни стен), измерение относится к код-ревью.
- Не связывался с владельцем — открытых продуктовых вопросов в ТЗ нет, и в ходе
ревью я не нашёл ни одного пограничного случая, требующего продуктового решения
(а не технического); единственная находка (L1) — техническая и снимается без
эскалации.
## Итог
ТЗ полностью соответствует PROCESS.md §7.1 и §2.4: обязательные разделы на месте,
диагноз проверен по коду и не является догадкой, AC однозначны и у каждого назван
способ доказательства, скоуп/не-скоуп непротиворечивы, откат и release-артефакты
описаны. Единственная находка — Low, снимается с запиской (L1). Задача может
двигаться в «Готово к разработке».