18 KiB
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/ — это выполнено.
Как проверялось
docs/SCOPE.md,AGENTS.md,PROCESS.md— рамка процесса и продукта.- Тело issue #289 и все три комментария (аналитика, продуктовый вопрос Q1 с default, готовность ТЗ) — сверил, что ТЗ реализует именно принятый default («запретить частичный сдвиг», не «разрезать запись»).
docs/RESIZE.mdиdocs/WALL-THICKNESS.md— канон подсистемы, на предмет расхождений терминологии и повторного изобретения уже существующих понятий.docs/USER-GUIDE.ru.md— сверка терминологии интерфейса (раздел «Resize», строки 458–474).- Само ТЗ (
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 расширяет уже обжитой класс тестов, а не создаёт первую регрессионную фикстуру с нуля.
- Не прогонялись
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) проверены точным чтением кода и канона и найдены корректными — рекомендую вернуть на правку только ради структурной полноты документа, не из-за ошибки в самом решении.