Files
houseplan-card/docs/reviews/SPEC-REVIEW-290-r1.md
T
2026-08-24 16:29:50 +03:00

266 lines
25 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-290-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/290
- **Этап:** spec (PROCESS.md §2.4)
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
- **Ревьюируемый артефакт:** `docs/specs/290-near-axis-authoring-and-repair.md`,
ветка `issue/290-near-axis-authoring-repair`, коммит `dcdb7565`
- **Вердикт:** жёлтый
## Скоуп ревью
Issue не помечен `small`/`trivial` → обычный трек, ТЗ обязано жить файлом в
`docs/specs/`. Диапазон изменений от `origin/dev` — ровно один файл
(`docs/specs/290-near-axis-authoring-and-repair.md`, класс C), что и требуется
на этапе `S4-spec-review`: продуктовый код не тронут.
Задача — часть цепочки #284 (канонизация координат записи) → #279 (устойчивый
рендер почти-ортогонального T-стыка) → #290 (не создавать такую геометрию
молча и дать явный способ её исправить). Продуктовая рамка по `docs/SCOPE.md`
— J6 («Keep the plan true as the home evolves»): администратор должен видеть и
контролировать изменение геометрии, Optimize обязан явно сообщать о
lossy-исправлениях. Это совпадает с собственной оценкой аналитика в issue.
## Как проверялось
1. Прочитаны `docs/SCOPE.md`, `PROCESS.md` (жизненный цикл, §7.1, §8),
`AGENTS.md`.
2. Прочитано тело issue #290 целиком и все три комментария (аналитика,
продуктовые вопросы с defaults, ссылка на готовое ТЗ).
3. Прочитан весь текст `docs/specs/290-near-axis-authoring-and-repair.md`
(313 строк).
4. Сверено с каноническими документами задетых подсистем: `docs/CANVAS.md`
(снап/Shift/hover-resolver, барьер канонизации координат записи, «Оптимизировать
планы»), `docs/RESIZE.md` (safe-resize contract, eligibility, pure pipeline,
Undo), `docs/WALL-THICKNESS.md` (модель `walls[]`, `wallKey`, rekey после
Resize/Optimize), `docs/USER-GUIDE.ru.md` (текущий текст диалога
«Оптимизировать планы» и его таблица операций).
5. Прочитаны связанные issue #279 (полная переписка причины/фикстуры),
#277 (safe Resize), #248 (идемпотентность Optimize), #284 (канонизация
записи) — все указаны в ТЗ как связанные.
6. Сверены существующие тестовые фикстуры: `test/fixtures/279-near-orthogonal-junction.json`,
`test/fixtures/real-plan-second-floor.json`, `test/fixtures/coordinate-canonicalization.json`
— на предмет соответствия тому, что AC4 называет «minimized fixture из #284».
7. Прочитан существующий продуктовый код на предмет уже введённой константы
`MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES = 0.25` (`src/wall-thickness.ts:84`),
чтобы проверить, не конфликтует ли предлагаемый общий `NEAR_AXIS_MAX_DEGREES`
с уже существующим порогом того же значения.
8. Для сравнения формата прочитаны полностью аналогичные недавние ТЗ той же
геометрической линии — `docs/specs/277-safe-resize.md` и
`docs/specs/279-near-orthogonal-junction.md` — как ориентир на «Риски»,
«Rollback» и формат таблицы AC/Доказательство, принятый в этом репозитории.
Гейты (`typecheck`/`test`/`build`) не запускались: на этапе `spec` продуктовый
код не менялся, диапазон diff состоит из одного файла документации, гоны не
применимы к этому этапу.
## Находки
### Medium (в скоупе задачи — чинится в этом же ТЗ)
**M1. Отсутствуют обязательные разделы §7.1 «риски» и «откат».**
- **Файл:** `docs/specs/290-near-axis-authoring-and-repair.md`
- **PROCESS.md §7.1:** «Обязательные разделы ТЗ: сценарий · что человек увидит
до и после · проблема · скоуп и не-скоуп · контракт поведения · UX · модель
данных и миграция · i18n · критерии приёмки AC1…ACn с указанием
доказательства · план автотестов · риски · откат · release-артефакты.»
- **Что не так:** в документе нет ни одного раздела «Риски» и ни одного
раздела/абзаца «Откат/Rollback». Единственное упоминание слова «риск» — в
шапке, это число самооценки аналитика (9/10), не анализ. Раздел 11
(«Release») описывает только трейлеры коммита и требование Linux-артефакта
для golden — про то, как вернуть изменение назад (revert commit? нужен ли
Labs-флаг? что произойдёт со старыми планами, если поведение окажется
ошибочным и придётся откатывать) не сказано ни слова.
- **Почему это не мелочь:** это не формальность, а действующая практика в этой
же геометрической линии задач. Оба непосредственных предшественника —
`docs/specs/277-safe-resize.md` (§12.1 «Риски и меры», таблица из 10 строк, и
§17 «Release-артефакты и rollback» с явным «Rollback — revert implementation
commit... После rollback вернётся широкий нестабильный UI, поэтому release
rollback должен быть полным и сопровождаться предупреждением») и
`docs/specs/279-near-orthogonal-junction.md` (§5 «Совместимость, риски и
performance») — оба содержат этот анализ. У #290 риск выше, чем у #277: здесь
вводится **автоматическое изменение геометрии при рисовании без обходного
модификатора** (§5.1 ТЗ, п. «Специального modifier bypass нет») — то есть
редактор будет менять то, что нарисовал пользователь, без явного согласия в
моменте. Ровно такой случай (переопределение поведения без spec на риски)
— то, ради чего DoR требует «влияние… названо (или явно «нет»)» и «риски
перечислены» (PROCESS.md §2.5).
- **Воспроизведение:** `grep -niE "риск|откат|rollback"` по файлу ТЗ (кроме
строки самооценки в шапке и одного упоминания «downgrade не требует
migration» в §9) не находит содержательного раздела.
- **Требуется:** добавить раздел «Риски» (минимум: ложные срабатывания
auto-straightening на длинных почти-диагональных стенах около границы
0.25°; двойной подсчёт shared-стены между Optimize-кандидатами; конфликт с
параллельно идущими #276/#278/#281, которые тоже трогают Optimize-геометрию,
как это явно оговорено в #277 §12.1) и раздел «Откат» (что происходит, если
после релиза найден дефект: revert коммита, нет миграции схемы, старые планы
с уступом остаются как есть до следующего Optimize).
**M2. AC исходного issue про `npm run invariants` на реальных данных и
видимое снижение числа почти-ортогональных узлов не перенесён в ТЗ.**
- **Файл:** `docs/specs/290-near-axis-authoring-and-repair.md`, раздел 8
(AC1–AC10), в сравнении с AC6 тела issue #290.
- **Что не так:** issue формулирует шестой критерий приёмки явно: «`npm run
invariants` на обоих реальных планах — код 0; после выпрямления уступа число
почти-ортогональных узлов уменьшается, и это видно в отчёте Optimize». В ТЗ
этому соответствует только AC5, но AC5 требует нулевых нарушений
`checkWallKeys`/`checkMixedRoleRecords`/preflight на **минимизированной
фикстуре**, а не на «обоих реальных планах», и не требует показать
уменьшение числа почти-ортогональных узлов ни в выводе
`scripts/model-invariants.mjs`, ни в `OptimizeReport`. Раздел 10
(«Ожидаемые файлы» → Tests/evidence) тоже не называет
`node scripts/model-invariants.mjs --config <...>` явно, хотя PROCESS.md §8
делает эту команду обязательной ровно для диффов, трогающих геометрию и
ссылки на неё (edges, `wall thickness records`, `open_spans`) — а это ядро
задачи #290.
- **Почему это не формальность:** минимизированная фикстура доказывает, что
механизм работает на сконструированном случае; она не доказывает, что на
реальном плане (с сотнями узлов, где уже случались #253/#258/#259) число
почти-ортогональных узлов действительно падает, а не создаётся новый класс
случаев в другом месте плана. Именно необходимость такой проверки на
реальных данных и породила #279 (реальный экспорт `44.json`) и практику
«второго реального плана» в текущей серии коммитов ветки
(`test: add a second real plan to the masonry gate`,
`test: one thickness record must not describe two wall roles`) — то есть
инфраструктура для анонимизированной проверки на реальных/близких к реальным
данным в этом репозитории уже есть и используется соседними задачами.
- **Требуется:** либо явно перенести требование issue в AC (анонимизированная
фикстура, произведённая из реального плана, а не полностью синтетическая
«minimized»; факт снижения near-orthogonal-count через
`scripts/model-invariants.mjs` до/после Optimize), либо явно и обоснованно
сузить критерий с указанием, почему полный реальный план как источник
проверки не нужен — так же, как #277 §16 прямо объясняет использование
анонимизированной регрессионной фикстуры вместо сырого экспорта. Молчаливое
сужение критерия приёмки, сформулированного в issue, без объяснения —
ровно тот случай, который ревью обязано ловить.
### Low (снимается либо правится, с записью)
**L1. AC4 неточно называет источник fixture.**
- **Файл:** `docs/specs/290-near-axis-authoring-and-repair.md`, AC4: «Minimized
fixture из #284 содержит duplicated shared physical edge `316×1`».
- **Наблюдение:** узел, который АС4 описывает (`316×1`, координаты
`(-1.670833333, 2.95)`/`(-0.354166667, 2.954166667)`), — это **буквально**
`test/fixtures/279-near-orthogonal-junction.json` (сверено байт в байт: тот
же `node`, те же координаты стен). Issue #284 — про барьер канонизации
координат записи (`snap = v => Math.round(v*240)/240`), отдельная задача про
другой механизм; она не производила эту fixture. Вероятная причина —
смешение «issue #284 как общее исследование, откуда взят пример» и «issue
#279 как задача, где эта fixture уже закоммичена». Это не блокирует
реализацию (fixture физически существует и полностью подходит под описание
AC4), но при написании автотеста первое, что сделает исполнитель — будет
искать/создавать fixture «из #284», где её нет.
- **Решение ревьюера:** Low, не эскалируется. Достаточно поправить ссылку на
`test/fixtures/279-near-orthogonal-junction.json` (или явно сказать «тот же
случай, что и в #279») при следующей правке файла; отдельного цикла ради
этого не требуется.
## Что проверено и признано корректным
- **Обязательные продуктовые разделы §7.1** («сценарий», «что человек
увидит») присутствуют и содержательны (разделы 1 и 3); ТЗ отвечает на оба
вопроса без терминов реализации в пользовательской части.
- **Продуктовые вопросы владельцу закрыты.** Ровно три вопроса
(выравнивать/предупреждать, поведение Optimize, единый допуск) заданы одним
комментарием с defaults, каждый — что человек видит/делает, не техническое
решение, выданное за продуктовое. Раздел 2 ТЗ фиксирует все три ответа
дословно, раздел «Открытых продуктовых вопросов нет» подтверждён — при
чтении issue и ТЗ вместе противоречий не найдено.
- **Единый допуск и его источник (раздел 4)** сформулирован проверяемо:
включительная граница `0.25°`, `316×1` классифицируется, `316×2` — нет,
zero-length исключён, инвариантность к endpoint order/winding/scale/theme
названа явно. AC1 требует единый source threshold и source-guard против
отдельных литералов `0.25` — реализуемо и falsifiable (мутант «порог ниже
`0.181315°`» и «strict `<`» в AC9 корректно бьют по обеим сторонам границы).
То, что предлагается использовать *то же числовое значение*, что уже занято
`MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES` в `src/wall-thickness.ts:84` для
другого геометрического сравнения (угол между двумя лучами, а не отклонение
одного луча от глобальной оси) — техническое решение, а не путаница: раздел
4 явно оговаривает, что renderer может продолжать использовать
«эквивалентный dot/sine form for orthogonal pairs», то есть общий источник —
это значение допуска, а не буквально один и тот же exported symbol. Это
подпадает под «принято предположительно, поменять свободно» и не требует
отдельного продуктового решения.
- **Негативный контракт (диагонали не трогаются)** описан симметрично
положительному на всех трёх путях (Walls, Resize, Optimize) и покрыт
отдельным AC6 плюс мутантами AC9 «считать room copies как две стены» /
«repair only one owner shared wall», что закрывает риск двойного счёта
расшаренной стены — именно то, из-за чего первоначальный AC issue выделял
этот пункт отдельно.
- **Совместимость с существующими контрактами подсистем.** Раздел 5.2
корректно наследует существующий контракт `docs/RESIZE.md` («только
numerically horizontal/vertical edge eligible», epsilon поглощает только
storage noise) и не пытается расширить Resize eligibility на near-axis
случаи — вместо этого явно направляет их через Optimize (раздел 12, пункт
2), что не противоречит ни `RESIZE.md`, ни `WALL-THICKNESS.md`. Раздел 6.1
корректно ссылается на существующий канонический механизм ключей толщины
(`wallKey`, exact-span lookup) из `WALL-THICKNESS.md` вместо изобретения
нового формата хранения — толщина не становится отдельным «candidate»,
а reprojection'ится из исправленной геометрии существующим pipeline, что
соответствует модели `WALL-THICKNESS.md` («key is computed from
lattice-stable endpoints»).
- **Touch/производительность (раздел 9)** соответствует терминологии
`docs/TOUCH-SUPPORT.md` дословно («View/kiosk rendering fully supported» ↔
«Fully supported» в таблице `TOUCH-SUPPORT.md`), не расширяет editor parity
на touch сверх «best effort», не блокирует pinch/pan/pointercancel —
соответствует установленному продуктовому решению («editors are
desktop-first»). Заявление «Authoring classifier `O(1)`» и «Optimize pass
линейный по edges» не противоречит существующим performance-бюджетам
`RESIZE.md` и не заявляет числ, которые нечем будет доказать: сохранена
ссылка на «full performance gate обязателен».
- **Никаких догадок, выданных за факт, не найдено** среди технических
утверждений о **существующем** поведении: каждая проверенная ссылка на
текущий контракт (Shift 45°, safe Resize eligibility, `wallKey`,
барьер канонизации записи #284) точно соответствует канону в `CANVAS.md` /
`RESIZE.md` / `WALL-THICKNESS.md`. Пункт раздела 12 («Принятые технические
предположения») корректно ограничен вопросами, которые пользователь не
наблюдает (какой endpoint двигается при равной безопасности кандидатов,
что unsafe-repair не обязан чинить любой ценой) — то есть именно тем
классом решений, которые PROCESS.md §7.1 разрешает решать автору/ревьюеру
без владельца.
- **i18n, DoR-чеклист.** Раздел 10 называет оба файла (`en.json`/`ru.json`);
конкретные ключи на этапе ТЗ не требуются — сверено с #277/#279, которые на
этом же этапе тоже ограничиваются общей фразой «RU/EN i18n» без перечисления
ключей.
- **Файл ТЗ лежит в правильном месте и правильно назван** —
`docs/specs/290-near-axis-authoring-and-repair.md`, номер совпадает с
номером issue, что соответствует PROCESS.md §2.3.
## Чего не проверял
- **Не проверял, что перечисленные в ТЗ файлы (`src/resize.ts`,
`src/align-grid.ts`/`src/plan-optimizer.ts`, `src/houseplan-card.ts`)
действительно являются правильными точками внедрения** — на этапе ТЗ кода
ещё нет, а раздел 10 явно помечен как «Ожидаемые файлы», то есть прогноз, а
не обязательство; проверка предметна для код-ревью.
- **Не оценивал реальную достижимость перфоманс-бюджетов** («Authoring
classifier O(1)», сохранение p95 из `RESIZE.md`) — это будет проверяться
performance-гейтом перед бетой, не на этапе спецификации.
- **Не проверял мутационный registry** (AC9) на предмет технической
реализуемости каждого конкретного мутанта в текущей кодовой базе — на этом
этапе mutation ids не существуют, это работа код-ревью.
- **Не запускал никаких гейтов** (`typecheck`/`test`/`build`) — diff этого
раунда состоит из одного файла документации класса C, гейты к этапу spec не
относятся (см. «Как проверялось»).
- **Не проверял docs/CONFIG-COMPATIBILITY.md registry на конкретные записи** —
ТЗ утверждает отсутствие изменений схемы (раздел 9), новых persisted-полей
нет, поэтому реестр совместимости не должен получать новых записей; это
утверждение принято на веру как техническое (не продуктовое) без
дополнительной проверки скрипта `config-field-registry.mjs`, так как задача
явно заявляет «schema не меняется».
## Вывод
Продуктовая часть ТЗ (сценарий, видимый результат, продуктовые решения
владельца, критерии приёмки как таковые, негативный контракт для диагоналей)
выполнена тщательно и без домыслов, выданных за факт. Причина жёлтого вердикта
— два содержательных пробела в обязательных разделах и трассируемости AC:
отсутствие анализа рисков и плана отката (§7.1), и молчаливое сужение AC6
issue (проверка на реальных данных, видимое снижение числа
почти-ортогональных узлов) без объяснения. Оба пункта — Medium в скоупе этой
же задачи и чинятся правкой того же файла, без нового issue и без обращения к
владельцу.