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

198 lines
18 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-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) проверены точным чтением кода и
канона и найдены корректными — рекомендую вернуть на правку только ради
структурной полноты документа, не из-за ошибки в самом решении.