diff --git a/docs/reviews/SPEC-REVIEW-273-r1.md b/docs/reviews/SPEC-REVIEW-273-r1.md new file mode 100644 index 00000000..76de580e --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-273-r1.md @@ -0,0 +1,92 @@ +# SPEC-REVIEW-273-r1 + +- Issue: [#273](https://github.com/Matysh/houseplan-card/issues/273) — Optimize сохраняет sub-grid островок толщины у T-узла: 22→15→22 +- Этап: ТЗ на ревью (`S4-spec-review`), заход **r1** (первый; предыдущих раундов нет) +- Артефакт ТЗ: `docs/specs/273-optimize-topology-island.md` +- Проверено на SHA: `b5fbe633a60f8a2091552204fc7ff9e3da67fb06` (HEAD ветки `issue/273-optimize-topology-island` на момент ревью) +- Лёгкий/короткий трек: нет (`small`/`trivial` не назначены) — ТЗ обязано жить файлом, что соблюдено +- Вердикт: **жёлтый** + +## Скоуп + +Явное расширение guard'а `collapseIsolatedWallThicknessIslands()` (#198, `src/plan-optimizer.ts:99-200`): разрешить схлопывание sub-half-grid островка толщины, когда ровно один его конец — реальный room/T topology node, а второй — синтетическая off-grid точка на том же прямом parent edge, при равных соседних толщинах. Изменение затрагивает только explicit Optimize; runtime/editor нормализация остаётся lossless (не входит в скоуп). + +Job по `docs/SCOPE.md`: **J6** («Keep the plan true as the home evolves», explicit maintenance должен приводить план к физически осмысленному и идемпотентному виду) — названо в ТЗ явно (§1), обоснование корректно. + +## Как проверялось + +1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком (обязательный порядок чтения). +2. Прочитано тело issue #273 и оба комментария (аналитика владельца, хендофф автора «ТЗ готово к ревью»). Второго ревью-раунда в истории issue нет — это первый заход, разбор полный, не по дельте (§2.10 неприменим). +3. Прочитан канонический документ подсистемы `docs/WALL-THICKNESS.md`, в частности §3 (уже документированный контракт #198 — «neither endpoint is a room vertex or resolved opening endpoint»), чтобы сверить, что ТЗ предлагает именно точечное расширение, а не переписывание контракта. +4. Прочитан сам ТЗ-файл `docs/specs/273-optimize-topology-island.md` целиком, сверены обязательные разделы §7.1 PROCESS.md. +5. Прочитан текущий код `src/plan-optimizer.ts` (`collapseIsolatedWallThicknessIslands`, guard `isTopologyNode`, сбор `nodes` из `roomPoly()` и `openCuts`) и `src/wall-thickness.ts` (`roomWallProfile`, `atomicPolyForRoom`, `floorFootprintGeometry` — комментарий «Independent partitions/columns are deliberately not accepted here»), чтобы проверить техническую состоятельность заявленных в ТЗ гарантий, а не поверить формулировкам на слово. +6. Сверены названные в ТЗ файлы/инструменты на существование: `demo/smoke_optimize_micro_interval.mjs`, `scripts/mutation-gate.mjs`, `wallBodiesGeometry()`, `docs/TESTING.md`, `docs/CONFIG-COMPATIBILITY.md` — все существуют, ссылки не фиктивны. +7. Сверен список «Обязательные регресс-тесты» из тела issue против AC1–AC9 ТЗ — построчное соответствие (см. таблицу ниже). +8. Гейты кода в этом раунде не гоняются: на этапе ТЗ нет кода для типecheck/test/build — это ожидаемо и не является упущением. + +### Соответствие регресс-тестов issue → AC ТЗ + +| Требование issue | AC ТЗ | +|---|---| +| 1. Minimized fixture `22→15→22`, T-узел слева, off-grid справа | AC1 | +| 2a. Оба конца топологические | AC3 | +| 2b. Opening endpoints | AC3 | +| 2c. Соседи различаются | AC4 | +| 2d. Conflicting exact owners | AC4 | +| 2e. Цепочка micro-островков | AC4 | +| 3. Идемпотентность, независимость от порядка/направления | AC5, AC6 | +| 4. Не только JSON — effective `wallIntervals` и render-толщина | AC1 (wallIntervals), AC2 (wallBodiesGeometry) | +| 5. Мутант старого `a \|\| b` guard обязан падать | AC8 | + +Все пункты, которые владелец назвал обязательными в issue, отражены в AC — пропусков нет. + +## Находки + +### Medium (в скоупе задачи) — заявленная защита от partition/draft-границы не имеет ни доказательства, ни видимого механизма реализации + +**Файл:** `docs/specs/273-optimize-topology-island.md`, §6.2 (пункт «второй endpoint не является... endpoint другого physical axis») и §6.3 (пункт «второй endpoint совпадает с отдельной room/partition/draft axis boundary»). + +**В чём проблема.** ТЗ формулирует это как обязательное блокирующее условие наравне с «оба конца топологические» и «opening/open-span endpoint» — но в отличие от них: + +- ни в AC3, ни в AC4 нет negative-фикстуры на этот случай (там перечислены только: оба topology/opening endpoints, разные соседи, missing/zero/open neighbor, chain/overlapping, conflicting owners, offset/perpendicular/parallel coincidence, intentional thickness change on topology boundary — партиций/drafts нет); +- текущий код не имеет механизма её обнаружить. `collapseIsolatedWallThicknessIslands()` строит список topology-узлов (`nodes`, `src/plan-optimizer.ts:111-119`) только из `roomPoly(room)` **всех** комнат и точек `openCuts` (это `open_spans`, а не двери/окна — терминология ТЗ здесь совпадает с существующими тестами #198, это я проверил отдельно и не считаю дефектом). Независимые перегородки (`space.partitions`) и drafts в эту функцию вообще не передаются как аргумент и нигде не читаются. Комментарий в `src/wall-thickness.ts:2311-2313` прямо формулирует принцип модуля: *«Independent partitions/columns are deliberately not accepted here, so they can never enlarge the slab perimeter»* — то есть весь код, от которого наследует `collapseIsolatedWallThicknessIslands`, сознательно не видит partition/draft-геометрию. + +**Конкретный сценарий поломки.** Перегородка обычно примыкает к стене комнаты не в вершине полигона комнаты, а в произвольной точке вдоль ребра (T-примыкание). Если синтетическая (некандидатная) граница `b` микро-островка на ребре комнаты случайно совпадёт именно с такой точкой примыкания перегородки — а не с вершиной какой-либо комнаты и не с концом `open_span` — сегодняшний `isTopologyNode` её не увидит вообще, ни до, ни после этого ТЗ: она не входит в `nodes` ни как room-vertex, ни как open-cut endpoint. Формулировка §6.3 обещает, что такой случай «всегда блокирует», но: (а) реализация этого не делает и в разделе «Ожидаемые файлы» (§10) нет ни новой структуры данных, ни изменения сигнатуры `collapseIsolatedWallThicknessIslands` для передачи partition/draft-геометрии; (б) ни один AC не проверяет этот случай, значит регресс тут никогда не будет пойман тестом даже если решение владельца — «пусть остаётся как есть». + +Это именно тот класс дефекта, о котором предупреждает процесс: *«утверждение о поведении, которого нет ни в одном документе и которое не помечено как предположение»*. Раздел §14 «Принятые технические предположения» перечисляет 5 пунктов и явно не содержит допущения по этому случаю — то есть автор не пометил его как предположение, а выдал как решённый и гарантированный факт. + +**Почему это не High.** Основной репродуцированный дефект (T-node одной комнаты + синтетическая точка на том же parent edge, без перегородок) специфицирован полно, тестируем и напрямую закрывает исходный баг-репорт — партиционный случай гипотетический, не из живого репро, и в худшем случае приводит не к потере геометрии, а к слиянию двух смежных data-записей с одинаковым `cm` в точке, где могла бы быть полезна отдельная граница для будущего редактирования. Это ухудшение data-model гранулярности, а не пользовательский регресс толщины/геометрии. + +**Что нужно поправить** (любое из трёх, на усмотрение автора — технический спор решается в следующем раунде, не эскалируется владельцу): +1. убрать пункт про partition/draft-boundary из §6.2/§6.3 как отдельную категорию и явно перенести его в §14 как «принято предположительно: коллизия с partition/draft-примыканием посередине ребра считается настолько редкой, что не тестируется в этом issue»; либо +2. добавить AC и negative-фикстуру, доказывающую, что такая коллизия действительно блокируется (и, если для этого нужно новое обнаружение — назвать это явно в §10 «Ожидаемые файлы», это меняет и оценку сложности); либо +3. явно доказать (комментарием в ревью-цикле), что геометрически такая коллизия на этом code path невозможна — если у автора есть довод сильнее прочитанного мной кода, он снимает находку. + +### Low — не все AC называют способ доказательства явным словом по шаблону §2.5 + +**Файл:** `docs/specs/273-optimize-topology-island.md`, §9, AC3, AC4, AC5, AC7. + +AC1/AC2/AC6/AC8 явно называют механизм доказательства (`test/plan-optimizer.test.mjs` + assertion / production-bundle smoke / mutation). AC3, AC4, AC5, AC7 описывают, что именно проверяется, но не произносят слово «unit» и не называют файл — это восстанавливается из контекста и §10 «Ожидаемые файлы» (`test/plan-optimizer.test.mjs`), поэтому не блокирует, но формально DoR требует «у каждого [AC] указано, чем он доказывается: unit / backend / smoke / golden / ревью кода» (§2.5). Рекомендация: при правке ТЗ по Medium-находке заодно дописать по одной строке «Доказательство: unit, `test/plan-optimizer.test.mjs`» к этим четырём AC — цена правки минимальна, откладывать в отдельный цикл не имеет смысла. + +## Что проверено и корректно + +- Все обязательные разделы §7.1 PROCESS.md присутствуют: сценарий (§1), что человек увидит до/после (§4), проблема и подтверждённая причина (§2–§3), скоуп/не-скоуп (§5), контракт поведения (§6), UX/accessibility/touch/security/perf (§8), модель данных и совместимость (§7, явно: без изменения `model_version`/схемы, старый клиент читает как один obычный wall entry), i18n (§8, явно «новых locale keys нет»), AC1–AC9 (§9), риски (§12), откат (§13), release-артефакты (§11). +- Технический контракт §6.1–6.2 (базовые условия #198 + разрешённый один T-endpoint) корректно описывает существующий код: реальный guard в `src/plan-optimizer.ts:155` — ровно `isTopologyNode(a) || isTopologyNode(b)`, как и заявлено в §3 «Подтверждённая причина»; арифметика порога (`GRID_PITCH = 4.166667`, `0.5 × GRID_PITCH = 2.083333 > 1.381904`) сходится с числами из реального экспорта. +- Термин «opening/open-span» в ТЗ корректно соответствует существующему словарю кода и тестов (`openCuts` = `open_spans`, не маркеры дверей/окон) — я отдельно перепроверил это по `test/plan-optimizer.test.mjs:236-266`, где `atOpening`-кейс тестирует именно `openCuts`; расхождения терминологии с каноном нет. +- Каждый обязательный регресс-тест из тела issue имеет соответствующий AC (таблица выше) — пропусков нет, автор не сузил скоуп относительно требования владельца. +- Не-скоуп (§5) корректно исключает runtime/editor cleanup, изменение persisted-схемы, #271 (finite-ray) — задачи не смешаны. +- Продуктовых вопросов владельцу нет, и это оправдано: единственная развилка («когда именно можно схлопывать») полностью решена техническим контрактом §6.1–6.3, разночтений в пользовательском сценарии я не нашёл. +- Rollback и release-артефакты реалистичны и не описывают несуществующую миграцию. +- Проверка «одно число — один источник» (PROCESS.md §8): новых пользовательских чисел не вводится, `wallsMerged` переиспользуется как есть (§7) — вопросов нет. + +## Чего не проверял + +- Не проверял фактическую реализацию (её ещё нет на этом этапе) — только заявленный контракт и его согласованность с текущим кодом `collapseIsolatedWallThicknessIslands`/`roomWallProfile`/`atomicPolyForRoom`. +- Не запускал `npm test`/`typecheck`/`build` — на этапе ТЗ гонять их не над чем, продуктовый код не менялся. +- Не проверял golden/smoke/performance — они относятся к код-ревью и пре-релизному гейту, не к ревью ТЗ. +- Не проверял, насколько часто на реальных планах перегородка примыкает к стене вне вершины комнаты (частотность сценария из Medium-находки) — эмпирических данных нет, вывод сделан только из чтения кода и комментариев модуля. +- Не проверял вручную конкретный приватный экспорт `1.json` — он не в репозитории, что соответствует правилу «не добавлять приватный файл»; воспроизвёл только числовую арифметику по описанию. + +## Унаследовано из r0 + +Неприменимо — это первый заход (r1), предыдущего цикла нет, наследовать нечего.