# 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 и без обращения к владельцу.