mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
committed by
Sergey Matyunin
parent
6a4e665d33
commit
8877e8c6d6
@@ -0,0 +1,197 @@
|
||||
# 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) проверены точным чтением кода и
|
||||
канона и найдены корректными — рекомендую вернуть на правку только ради
|
||||
структурной полноты документа, не из-за ошибки в самом решении.
|
||||
Reference in New Issue
Block a user