Files
houseplan-card/docs/reviews/SPEC-REVIEW-289-r1.md
T
2026-08-24 15:19:20 +03:00

18 KiB
Raw Blame History

SPEC-REVIEW-289-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/289
  • Артефакт ТЗ: docs/specs/289-no-mixed-role-resize.md
  • Ветка/коммит: issue/289-no-mixed-role-resize @ 5e169f48 (origin/dev + 1 коммит, подтверждено git diff origin/dev..HEAD --stat — единственное изменение это сам файл ТЗ, 234 строки; продуктовый код не тронут)
  • Заход: r1 (первый прогон, раздел «дельта/унаследовано» не применяется — §2.10 PROCESS.md)
  • Вердикт: жёлтый · High: 0 · Medium: 3 · Low: 2

Скоуп ревью

ТЗ описывает продуктовое поведение: Resize не должен позволять жесту превратить часть ранее общей боковой стены в наружную (или наоборот) без разреза записи толщины — вместо этого рукоятка должна быть заранее disabled с понятной причиной. Задача не small (меток small/trivial на issue нет), значит ТЗ обязано жить файлом в docs/specs/ — это выполнено.

Как проверялось

  1. docs/SCOPE.md, AGENTS.md, PROCESS.md — рамка процесса и продукта.
  2. Тело issue #289 и все три комментария (аналитика, продуктовый вопрос Q1 с default, готовность ТЗ) — сверил, что ТЗ реализует именно принятый default («запретить частичный сдвиг», не «разрезать запись»).
  3. docs/RESIZE.md и docs/WALL-THICKNESS.md — канон подсистемы, на предмет расхождений терминологии и повторного изобретения уже существующих понятий.
  4. docs/USER-GUIDE.ru.md — сверка терминологии интерфейса (раздел «Resize», строки 458–474).
  5. Само ТЗ (docs/specs/289-no-mixed-role-resize.md), построчно, с проверкой каждого утверждения о текущем поведении по коду:
    • src/resize.ts: resolveSafeResize() (строки 629–728) и validateSafeResize() (791–886) — чтобы проверить заявление §1 «текущий resolver проверяет ownership только moving edge, не двух side edges»;
    • scripts/model-invariants.mjs: checkMixedRoleRecords, checkWallKeys, checkWallRecordsPreserved, checkReferences, checkPhysicalGeometry — чтобы проверить, что все инструменты, названные в AC5, существуют и возвращают то, что от них ожидает ТЗ (в частности, что checkWallKeys действительно никогда не кладёт результат в violations, а только в notes — семантика «ноль нарушений» в AC5 подтвердилась);
    • src/i18n/en.json / ru.json — существование ключей resize.disabled.partial-shared и resize.commit_failed, чтобы отличить переиспользование от изобретённого нового поведения;
    • test/fixtures/resize-safe-regression.json — существующая фикстура, упомянутая в docs/RESIZE.md, для понимания, что #289 расширяет уже обжитой класс тестов, а не создаёт первую регрессионную фикстуру с нуля.
  6. Не прогонялись typecheck/test/build/инварианты: диапазон коммитов на ветке не содержит продуктового кода (см. п. «Ветка/коммит» выше) — гейты класса A/B не применимы к чисто документационному коммиту на этапе ТЗ.

Находки

M1 (Medium, в скоупе) — раздел «Риски» отсутствует полностью

§7.1 PROCESS.md перечисляет «риски» как обязательный раздел ТЗ; DoR (§2.5) отдельно требует «риски перечислены» до перехода в «Готово к разработке». В документе слово «риск» встречается только один раз — в шапке, как оценочная цифра S2-аналитики («риск 9/10»), скопированная из комментария аналитика. Ни одного предложения о том, какие риски несёт именно это решение — например, что более строгий ownership-анализ может задеть легитимные resize-жесты на грани эвристики (ложноположительный partial-shared), или что directed-clamp по двум side edges одновременно — новый код на горячем пути pointermove, — не написано. Раздел docs/RESIZE.md §Performance частично закрывает производительность («Pointermove не получает новый глобальный O(R×E) анализ»), но это не заменяет отдельный разбор рисков внедрения самой проверки.

Как закрыть: добавить раздел «Риски» — минимум: риск чрезмерно строгого disable (ложноположительные срабатывания на legit-жестах, покрывается AC3/AC4/ AC8), риск регресса производительности на большом плане (покрывается docs/RESIZE.md p95-бюджетами, но это нужно явно связать), риск того, что ownership-профиль устареет при вложенном/составном изменении темы (multiple- rooms уже existing stop).

M2 (Medium, в скоупе) — раздел «Откат» отсутствует полностью

Тот же §7.1 требует явный раздел «откат»; DoR требует «откат: как выключить или вернуть назад (флаг Labs, обратная миграция)». В документе нет ни слова «откат», ни эквивалентного рассуждения. Поскольку задача не меняет схему/ миграцию (§7 «Persisted schema/model version не меняются»), ответ, вероятно, тривиален — «отката как флага не требуется, откат это revert коммита, т.к. persisted-данные не переписываются и разрешённые сценарии не деградируют» — но это решение должно быть записано явно, а не додумываться ревьюером или разработчиком.

M3 (Medium, в скоупе) — 6 из 9 AC не указывают способ доказательства

§7.1 требует «критерии приёмки AC1…ACn с указанием доказательства»; DoR дублирует это отдельным пунктом. AC7 явно называет смок (demo/smoke_room_resize.mjs), AC8 явно называет мутационный тест, AC9 явно перечисляет гейты — но AC1–AC6 описывают только ожидаемое поведение (что должна вернуть функция, что должно остаться неизменным), не говоря, каким именно тестом/каким инструментом это доказывается. Способ вывести это из контекста существует (все они — чистые функции, естественная площадка — test/resize.test.mjs, как и для существующих сценариев в docs/RESIZE.md §Verification), поэтому неоднозначности в реализуемости нет — но формальное требование не выполнено систематически, не разово, и я обязан отметить это явно, а не «додумать за автора». Тривиально чинится: одна строка на AC вида «Доказательство: unit, test/resize.test.mjs» / «инвариант, npm run invariants».

Итог по Medium: все три — редакционные/структурные пробелы конкретно в этом документе, ни один не требует нового продуктового решения владельца и не меняет контракт §2/§4 — правятся тем же автором в рамках этого же issue, отдельный issue не заводится (#202).

L1 (Low) — терминология «рукоятка» вместо канонической «ручка»

docs/USER-GUIDE.ru.md:467 и уже существующие строки i18n (title.markup_resize, markup.hint_resize) называют элемент управления Resize «ручка». Слово «рукоятка» в репозитории отсутствует везде, кроме этого нового ТЗ, где оно использовано 5 раз (§2, §3, §4.2×3). AGENTS.md прямо требует: «для работы, меняющей видимое поведение, читать docs/USER-GUIDE.ru.md — терминология интерфейса берётся оттуда, а не изобретается, или UI начинает говорить на языке разработчика». Сам disabled- текст в AC2 («Нельзя сдвинуть только часть общей стены») дословно совпадает с решением владельца и дефекта не содержит — расхождение только в описательной прозе ТЗ, но именно она станет источником терминологии для реализации и код-ревью. Рекомендация: заменить «рукоятка» → «ручка» по всему документу.

L2 (Low) — i18n-ключ не назван по имени

§10, предположение 1: «существующий reason key partial-shared переиспользуется» — это ключ значения reason в SafeResizeResolution (src/resize.ts:37), а не сам i18n-ключ перевода. Реальный ключ перевода, который получит новый текст — resize.disabled.partial-shared (подтверждено в src/i18n/en.json:90, ru.json:90) — нигде в документе не назван буквально. DoR требует «i18n: ключи en + ru перечислены». Поскольку ключ уже существует (не создаётся новый), риск неоднозначности невелик, но раздел должен явно назвать его, а не полагаться на то, что разработчик найдёт его сам по строке reason.

Что проверено и корректно (не находка, а подтверждение)

  • Причина дефекта (§1) фактически точна. Прочитал resolveSafeResize() целиком: ownership-проверка (partial/unequal/exact, строки 660–677) выполняется только для moving edge. validateSafeResize() проверяет для side edges лишь ось (sideAxis) и посадку проёмов (sideOpeningFits), но не сравнивает получившийся side-интервал с геометрией соседних комнат. Значит утверждение «resolver не доказывает роль side edges после изменения длины» — не догадка, а точное описание кода.
  • Симметричность контракта §4.2 проверена геометрически. Прогнал вручную сценарий из репро: при удлинении side-стены новый хвост становится наружным (mixed-role на записи этой комнаты) — очевидная часть. Менее очевидная — что при укорачивании (обратное направление) mixed-role возникает не у текущей комнаты, а у соседней (её длинный shared-участок теряет часть партнёра и должен разделиться на shared+outer). Именно это покрывает второй пункт §4.2: «оставляет продолжение у B, которым A больше не владеет». Формулировка «оба направления небезопасны» в exact-репро — корректна, не преувеличение.
  • Инструменты и ключи, упомянутые в AC, существуют и делают ровно то, что написано: checkMixedRoleRecords, checkWallRecordsPreserved, checkWallKeys, checkReferences, checkPhysicalGeometry (scripts/model-invariants.mjs), resize.commit_failed (src/houseplan-card.ts:8636, оба i18n-файла). Ничего не изобретено.
  • Скоуп/не-скоуп (§5) не расползается: явно исключены разрез записи, смена толщины, каскад топологии, рефактор #264, ретро-починка через Optimize, #288 и #290 — совпадает с картиной, полученной из истории issue (дубликаты проверены аналитиком: #233, #253, #264, #277, #281, #287).
  • Продуктовое решение (§2) совпадает слово в слово с ответом владельца на Q1 (default «запретить», без auto-split, с текстом «Нельзя сдвинуть только часть общей стены») — открытых продуктовых вопросов действительно не осталось, значит вынесение владельцу новых вопросов не требуется.
  • Терминология вне «рукоятки» (endpoint-to-endpoint, geometry preflight, safety floor, centreline) сверена с RESIZE.md/WALL-THICKNESS.md/ TOUCH-SUPPORT.md/ARCHITECTURE.md — совпадает с каноном, ничего не изобретено заново.
  • Совместимость (§7): утверждение «persisted schema/model version не меняются» соответствует docs/CONFIG-COMPATIBILITY.md-классу решений — задача не требует миграции, что снимает часть требований DoR (но не заменяет отсутствующий явный раздел «откат», см. M2).

Чего не проверял

  • Продуктовый код и тесты — на этой ветке их не существует (это стадия ТЗ, git diff origin/dev..HEAD содержит только сам файл спецификации). Гейты typecheck/test/build/invariants не прогонялись: неприменимо к чисто документационному коммиту, и §1 PROCESS.md не требует их для класса C.
  • Английский текст disabled-причины — ТЗ говорит только «EN передаёт тот же смысл» и сознательно не фиксирует точную строку; это оставлено как редакционная свобода реализации (аналогично другим reason-текстам в en.json), не считаю это пробелом.
  • Содержимое будущей анонимизированной фикстуры из #284 (файл ещё не создан) — не могу проверить, действительно ли она будет содержать узлы 928/971 и координаты y=3.866666667 → 4.045833333 из issue; это дело реализации и код-ревью, а не спецификации.
  • Производительность на реальном плане владельца — числа p95 в docs/RESIZE.md не пересчитывались; спецификация лишь ссылается на них, что на этой стадии достаточно.

Итог

High: 0. Medium: 3 (M1 «Риски», M2 «Откат», M3 — доказательство по AC), все три — редакционные пробелы этого самого документа, чинятся автором ТЗ в рамках текущего issue без нового продуктового решения владельца. Low: 2 (терминология «рукоятка»/«ручка», явное имя i18n-ключа) — либо правятся тем же проходом, либо снимаются автором с запиской. Технический контракт (§2–§4), AC1–AC9 по существу и границы скоупа (§5) проверены точным чтением кода и канона и найдены корректными — рекомендую вернуть на правку только ради структурной полноты документа, не из-за ошибки в самом решении.