docs: review document for #290

Issue: #290
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-24 16:29:50 +03:00
committed by Sergey Matyunin
parent 6e1007a363
commit 7fd9ca0f6a
+265
View File
@@ -0,0 +1,265 @@
# 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 и без обращения к
владельцу.