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

17 KiB
Raw Blame History

SPEC-REVIEW-253-r1

  • Issue: #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). Задача может двигаться в «Готово к разработке».