From 62dd02d8234a8d5b9b629e2fd77b1c09f00932ab Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Mon, 24 Aug 2026 01:04:17 +0300 Subject: [PATCH] docs: constrain multi-wall containment to rectilinear nodes Issue: #275 User-Visible: no --- docs/specs/275-multiwall-strip-containment.md | 140 ++++++++++++------ 1 file changed, 93 insertions(+), 47 deletions(-) diff --git a/docs/specs/275-multiwall-strip-containment.md b/docs/specs/275-multiwall-strip-containment.md index 2d54d969..3f205d75 100644 --- a/docs/specs/275-multiwall-strip-containment.md +++ b/docs/specs/275-multiwall-strip-containment.md @@ -6,7 +6,7 @@ сложность 8/10 · риск 9/10 - Issue: [#275](https://github.com/Matysh/houseplan-card/issues/275) - Ветка: `issue/275-multiwall-strip-containment` -- Статус ТЗ: готово к ревью +- Статус ТЗ: редакция r2 после авторского контрпримера к r1, готово к ревью Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md`, `docs/USER-GUIDE.ru.md` и `docs/TESTING.md`. @@ -96,8 +96,20 @@ semantic inventory и golden стали зелёными, хотя пользо Тесты #272 проверяли только связность пустоты, single retained/discarded probes и синтетические cases. Code review прямо не сверял cases с приватными экспортами и не делал ручной UI-check. Ни один gate не проверял отношение -`incident strip union ⊆ canonical masonry`, поэтому релизный escape был -возможен. +сохранение прямоугольного T/X overlap, поэтому релизный escape был возможен. + +После зелёного spec review r1 автор исполнил буквальную формулу +`incident strip union ⊆ canonical masonry` на неортогональном fixture #249. +Она вернула в masonry уже запрещённый `discardedWedgeProbe` +`(330.3808442725, 148.8560107825)`: точка лежит внутри двух raw incident +rectangles, хотя утверждённая владельцем локальная фаска обязана оставить её +пустой. Поэтому общий для всех multi-wall nodes strip-containment неверен. + +Новые backups показывают другой, более узкий класс: все 12 измеренных потерь +лежат в **rectilinear** T/X nodes, где направления попарно параллельны либо +перпендикулярны. У такого стыка raw rectangular overlaps и есть правильная +кладка; pairwise bevel не решает spike, а создаёт выемку. Неортогональный узел +#249 сохраняет прежний bounded-bevel contract. ## 4. Что человек увидит до и после @@ -105,10 +117,10 @@ semantic inventory и golden стали зелёными, хотя пользо толщина входящего отрезка визуально уменьшается, хотя editor overlay показывает полную ось. -**После:** каждый конечный входящий отрезок сохраняет заданную полную толщину -до node. Стык заполнен. Если room-ring действительно образовала внешний -mitre-spike вне всех физических strips, он по-прежнему срезан прямой фаской -#249 и не выходит за `R`. +**После:** в прямоугольном T/X каждый конечный входящий отрезок сохраняет +заданную полную толщину до node, а стык заполнен без pairwise cut. В +неортогональном node прежняя прямая фаска #249 по-прежнему удаляет внешний +mitre-spike и не выходит за `R`. Optimize валидного плана может остаться no-op. Raw, Preview, Apply, reload, Plan/View/Static/Iso и световая модель показывают одну форму. @@ -117,10 +129,11 @@ Plan/View/Static/Iso и световая модель показывают од ### Входит -- канонический containment-инвариант для положительных finite incident strips; -- ограничение bevel-cut только материалом, не принадлежащим ни одной incident - strip; -- rectilinear T/X и неортогональный fixture #249, равные/смешанные толщины, +- каноническая классификация rectilinear multi-wall nodes; +- запрет pairwise bevel-cuts внутри rectilinear T/X и сохранение полного union + их положительных finite incident strips; +- неизменность неортогонального bounded bevel #249; +- rectilinear T/X и regression fixture #249, равные/смешанные толщины, короткие rays, `cell_cm: 1/5/30`; - `roomGeom`, final masonry, paper, clean floor/fill/hover, Plan/View/kiosk, Static, hidden Iso и light/sun barriers; @@ -140,7 +153,24 @@ Plan/View/Static/Iso и световая модель показывают од ## 6. Канонический геометрический контракт -### 6.1 Incident strip +### 6.1 Классификация rectilinear node + +Для canonical multi-wall node берутся направления его положительных finite +rays после действующей дедупликации. Node является `rectilinear`, если для +каждой пары направлений нормализованный модуль scalar product с единым +angle-epsilon означает одно из двух: + +- направления параллельны/противоположны: `|dot| ≈ 1`; +- направления перпендикулярны: `|dot| ≈ 0`. + +Иными словами, все rays принадлежат двум взаимно перпендикулярным +неориентированным осям. T имеет три directed rays, X — четыре. Классификация не +зависит от winding, reversed endpoints, room/wall order и `coordScale`. +Произвольно близкий, но физически диагональный луч не округляется к 90°: +epsilon компенсирует только storage/normalization noise и покрывается +пограничными негативными тестами. + +### 6.2 Incident strip rectilinear node Для каждого canonical multi-wall node и каждого `ray.support` определяется конечная полоса: @@ -152,9 +182,10 @@ Plan/View/Static/Iso и световая модель показывают од - duplicate room owners схлопываются действующей детерминированной картой. `requiredStripUnion(node)` — boolean union этих полос в действующей node mask. -Он является физическим минимумом masonry до применения явных opening cuts. +Для rectilinear node он является физическим минимумом masonry до применения +явных opening cuts. -### 6.2 Containment +### 6.3 Rectilinear containment До opening cuts должно выполняться: @@ -167,43 +198,53 @@ requiredStripUnion(node) − roomGeom = ∅ подтверждённому opening slot. Pairwise bevel, room-ring winding и paper repair не имеют права удалять остальную часть incident strip. +`multiWallBevelCutsAt()` не создаёт cut ни для одной пары rectilinear node. +Локальная реконструкция сохраняет union finite supports и node core. Это +правило относится ко всему rectilinear node целиком: недостаточно пропустить +только одну 90° пару и оставить соседний cut того же T/X. + Containment проверяется площадью vector difference и устойчивыми interior samples. Одной точки node, числа polygon holes либо connected-component классификации пустоты недостаточно. -### 6.3 Bounded exterior bevel +### 6.4 Неортогональный bounded bevel -Room contours могут содержать material за `R`, который не принадлежит ни одной -incident strip. Только такой material разрешено удалить bevel-cut: +Если node не прошёл rectilinear-классификацию, действующий контракт #249 не +расширяется и не заменяется strip-containment: -```text -effectiveCut = pairwiseCut − requiredStripUnion(node) -``` +- pairwise offset intersections за `R` по-прежнему дают прямую фаску; +- `discardedWedgeProbe` остаётся пустым, даже если он попадает в overlap raw + endpoint rectangles; +- retained wedge #261, node connectivity и finite support bounds #271 + сохраняются; +- исправление #275 не меняет форму, площадь либо path fixture #249 сверх + неизбежного floating-point normalization. -Либо технически эквивалентной операцией, если доказаны те же множества. -Existing excessive-wedge probe #249 остаётся пустым, каждая join-вершина -остаётся в bound, но прямоугольные T/X не получают выемку в overlap strips. +Так явно разрешается конфликт r1: полная поперечная strip у математического +endpoint является обязательной только для rectilinear стыка. В +неортогональном локальном join её угловая часть может принадлежать утверждённой +фаске #249. -### 6.4 Paper, floor и physics +### 6.5 Paper, floor и physics Paper покрывает room centre и canonical masonry. Он не может ни скрывать отсутствующую masonry белой подложкой, ни повторно вырезать required strip. Clean floor не присваивает эту площадь комнате. Light/sun barriers, hidden Iso и SVG используют один corrected structural result, без surface-only patch. -### 6.5 Openings и independent bodies +### 6.6 Openings и independent bodies -Room opening cuts выполняются после strip-safe node reconstruction по +Room opening cuts выполняются после rectilinear-safe node reconstruction по существующей association. Independent partitions/drafts/columns объединяются в существующем порядке. Тест отличает разрешённый opening slot от bevel-loss и не объявляет любой белый sample допустимым только из-за близкого opening. -### 6.6 Failure isolation и determinism +### 6.7 Failure isolation и determinism -Неуспешный optional local cut не гасит весь план и не обходит containment -fallback. Rooms/walls order, reversed endpoints, winding, duplicate owner, -raw/optimized/reloaded storage не меняют semantic result. Product render не -мутирует config. +Неуспешный optional local cut не гасит весь план. Rectilinear node не может +молча вернуться к pairwise cuts как failure fallback. Rooms/walls order, +reversed endpoints, winding, duplicate owner, raw/optimized/reloaded storage +не меняют classification и semantic result. Product render не мутирует config. ## 7. Совместимость, UX, security и performance @@ -211,17 +252,18 @@ raw/optimized/reloaded storage не меняют semantic result. Product render - Legacy/canonical keys используют resolved `WallInterval` как сейчас. - Новых controls, touch/keyboard/focus/ARIA и locale keys нет. - Новых HA calls, URL/HTML, permissions и security boundaries нет. -- Required-strip geometry строится внутри cached structural node pass. Новый +- Rectilinear classification и required-strip geometry строятся внутри cached + structural node pass. Новый глобальный `O(E²)` обход и пересчёт на HA/theme/hover tick запрещены. - Local masks и число rays одного node ограничивают boolean work; benchmark large-house не должен выйти за действующий prerelease budget. ## 8. Acceptance criteria и доказательства -### AC1. Оба новых реальных класса дают нулевую потерю strips +### AC1. Оба новых rectilinear класса дают нулевую потерю strips Минимизированные анонимные fixtures содержат точные координаты/толщины минимум -узлов `(204.166667, 645.833333)` при `cell_cm: 5` и +rectilinear-узлов `(204.166667, 645.833333)` при `cell_cm: 5` и `(-354.166667, 2087.5)` при `cell_cm: 1`, плюс по одному треугольному T из каждого backup. Для каждого: @@ -234,12 +276,13 @@ raw/optimized/reloaded storage не меняют semantic result. Product render ### AC2. #249 остаётся ограниченным -На fixture #249 old excessive-wedge area, не принадлежащая incident strips, -остаётся пустой; join не выходит за `R + epsilon`. Retained wedge #261 заполнен, +Fixture #249 классифицируется как non-rectilinear. Его existing +`discardedWedgeProbe` остаётся пустым, join не выходит за `R + epsilon`, а +normalized geometry area/path не меняются. Retained wedge #261 заполнен, finite endpoints #271 не удлиняются, zero/open ray не материализуется. -**Доказательство:** существующие probes плюс vector difference между pairwise -cut и required strips. +**Доказательство:** существующие probes, classification unit и geometry +difference до/после #275 для fixture #249. ### AC3. Exact-input lifecycle проходит на полных backups владельца @@ -247,7 +290,8 @@ cut и required strips. проверяет четыре состояния: raw render, Optimize Preview, Apply + backend echo, reload + повторный render. Для обоих файлов: -- нарушений strip containment — `0` во всех degree-3+ nodes; +- нарушений strip containment — `0` во всех rectilinear degree-3+ nodes; +- non-rectilinear nodes сохраняют утверждённые #249/#261 probes; - Optimize не мутирует preview input; - `2.json` вправе остаться `changed: false`, но render уже корректен; - targeted crops не содержат прежних белых треугольников/крупного провала. @@ -266,7 +310,7 @@ structural cache и не меняют path/fingerprint. ### AC5. Visual gate видит именно выемку, а не только enclosed hole -Golden/local crop semantic preflight проверяет required-strip containment до +Golden/local crop semantic preflight проверяет rectilinear strip containment до pixel diff. Предрелизный baseline не может пройти, если пустота связана с внешним фоном, но лежит внутри incident strip. Hole inventory #272 остаётся дополнительной, а не достаточной проверкой. @@ -275,8 +319,8 @@ pixel diff. Предрелизный baseline не может пройти, ес ### AC6. Мутант ловит release escape -Исполняемый mutation возвращает безусловное вычитание полного pairwise cut из -ray-strip union. AC1 либо AC5 краснеют, даже если legacy +Исполняемый mutation отключает rectilinear guard и возвращает pairwise cuts в +обычные T/X. AC1 либо AC5 краснеют, даже если legacy `enclosedHoles === 0`, retained probe и discarded probe остаются зелёными. **Доказательство:** отдельная запись `scripts/mutation-gate.mjs`, прогнанная в @@ -286,8 +330,9 @@ ray-strip union. AC1 либо AC5 краснеют, даже если legacy В репозитории отсутствуют полные backups, layout и пользовательские названия. Permutation rooms/walls, reversed interval endpoints, повторный расчёт, -canonical storage echo и оба grid scales дают одинаковый containment result и -не мутируют input. +canonical storage echo и оба grid scales дают одинаковую classification и +containment result, не мутируя input. Негативная angle-boundary matrix не +переклассифицирует диагональный fixture в rectilinear. **Доказательство:** table-driven units и проверка Git diff/fixture contents. @@ -336,7 +381,7 @@ smoke, performance и exact-SHA Validate. | Риск | Мера | |---|---| -| Возвратить spike, лишь перестав вычитать cuts | AC2 проверяет area вне strips и bound `R`. | +| Возвратить spike глобальным отключением cuts | Rectilinear guard + AC2 сохраняют non-rectilinear geometry #249. | | Заполнить настоящий opening | AC1/AC3 различают pre-opening `roomGeom` и разрешённый slot. | | Починить только SVG | AC4 проверяет paper/floor/Iso/light из общего source. | | Снова принять похожую синтетику | AC1 требует точные minimized coordinates обоих backups, AC3 — полные файлы. | @@ -354,8 +399,9 @@ baseline при старом безусловном cut либо вернуть 1. Продуктовый ответ однозначен из репорта владельца: сохранённая положительная полоса стены не может быть вырезана renderer-bevel. -2. Различающая формула `effectiveCut = pairwiseCut − requiredStripUnion` задаёт - множество, а не обязательную реализацию; эквивалентный алгоритм допустим. +2. Исправление классификационное, а не глобальное: rectilinear T/X не получает + pairwise cuts; non-rectilinear node сохраняет #249. Общий union strips для + всех nodes запрещён контрпримером r1. 3. Optimize для валидной осевой модели не обязан менять данные ради renderer workaround. 4. Белые треугольники, потеря толщины и крупная выемка имеют одну доказанную