From 762e9f4b323d9d00ae4b230e419b281cd3d82a9a Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Mon, 24 Aug 2026 00:55:02 +0300 Subject: [PATCH] docs: specify multi-wall strip containment Issue: #275 User-Visible: no --- docs/specs/275-multiwall-strip-containment.md | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 docs/specs/275-multiwall-strip-containment.md diff --git a/docs/specs/275-multiwall-strip-containment.md b/docs/specs/275-multiwall-strip-containment.md new file mode 100644 index 00000000..2d54d969 --- /dev/null +++ b/docs/specs/275-multiwall-strip-containment.md @@ -0,0 +1,364 @@ +# Issue #275 — multi-wall bevel не вырезает реальные полосы стен + +- Дата: 2026-08-24 +- Тип: regression bug · приоритет P1 +- Оценка: пользовательская ценность 10/10 · ценность для разработки 10/10 · + сложность 8/10 · риск 9/10 +- Issue: [#275](https://github.com/Matysh/houseplan-card/issues/275) +- Ветка: `issue/275-multiwall-strip-containment` +- Статус ТЗ: готово к ревью + +Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`, +`docs/WALL-THICKNESS.md`, `docs/USER-GUIDE.ru.md` и `docs/TESTING.md`. + +Связанные задачи: +[#249](https://github.com/Matysh/houseplan-card/issues/249), +[#261](https://github.com/Matysh/houseplan-card/issues/261), +[#270](https://github.com/Matysh/houseplan-card/issues/270), +[#271](https://github.com/Matysh/houseplan-card/issues/271), +[#272](https://github.com/Matysh/houseplan-card/issues/272) и +[#273](https://github.com/Matysh/houseplan-card/issues/273). + +## 1. Сценарий и персона + +Администратор обновляет карточку до beta.6, нажимает Optimize и открывает два +обычных жилых плана. В прямоугольных T-стыках остаются белые треугольники, +часть стены выглядит тоньше заданной, а в одном смешанном по толщине стыке +видна крупная белая выемка. Оси и узлы редактора при этом показывают +непрерывные конечные отрезки. + +Это нарушает J1 и J6 из `docs/SCOPE.md`: главный план показывает отсутствующий +проём, а masonry, paper, floor и light получают одну и ту же неверную +вычисляемую геометрию. Дефект выпущен после задач #271–#273, поэтому исправление +и тесты обязаны доказываться на тех же полных входах, а не только на похожей +синтетике. + +## 2. Подтверждённое воспроизведение + +Владелец повторно прислал два plan-only backup после beta.6: + +| Вход | Масштаб | Rooms / walls | Degree-3+ nodes | Nodes с потерей incident strip | +|---|---:|---:|---:|---:| +| `1.json` | `cell_cm: 5` | 8 / 22 | 12 | 4 | +| `2.json` | `cell_cm: 1` | 8 / 25 | 14 | 8 | + +Оба файла прогнаны цепочкой production pure-функций: +`spaceModels()` → `wallIntervals()` → `buildMultiWallNodeMap()` → +`wallBodiesGeometry()`. В локальном окне каждого node семплируется union его +конечных положительных ray-support strips. Beta.6 возвращает точки, которые +лежат внутри сохранённой strip, но отсутствуют в `roomGeom`. + +Измеренные примеры: + +- `1`, `(204.166667, 645.833333)`: 391 потерянный sample на локальной сетке; +- `2`, `(-354.166667, 2087.5)`: 242 sample — крупный видимый провал; +- `2`, `(-354.166667, 2287.5)`, `(1058.333333, 2287.5)`, + `(2404.166667, 1245.833333)` и `(2404.166667, 2308.333333)`: устойчивые + треугольные вырезы внутри реальных полос. + +Повторный Optimize разделяет источник дефекта: + +- для `2.json` результат законно `changed: false`; +- для `1.json` канонизируются storage-хвосты, но сохраняются те же четыре + renderer-нарушения; +- сериализация/reload не меняют число нарушений. + +Persisted оси и wall intervals валидны. Optimize не должен угадывать новую +толщину либо удалять координаты; исправляется вычисляемая renderer geometry. + +Полные backups, layout и названия комнат приватны и в Git не добавляются. +Локальный exact-input gate использует файлы владельца вне репозитория, а в Git +попадают только минимизированные анонимные topology fixtures. + +## 3. Подтверждённая причина и почему прошли тесты beta.6 + +#249 ввела bounded multi-wall bevel. Когда pairwise пересечение смещённых +граней дальше `R = 1.25 × H`, `multiWallBevelCutsAt()` строит треугольный cut. + +#272 заметила замкнутые белые полости, но приняла слишком слабый инвариант: +`enclosedHoles === 0`. Реализация добавила к cut corridor, связывающий его с +внешней пустотой. Замкнутая белая полость стала открытой белой выемкой, поэтому +semantic inventory и golden стали зелёными, хотя пользовательский дефект +остался видимым. + +`bevelMultiWallBody()` затем: + +1. вычитает полный pairwise cut из исходной room-ring masonry; +2. восстанавливает union прямоугольников конечных ray supports; +3. снова вычитает pairwise cut уже из этого физического union. + +В обычном прямоугольном T пересечение offset faces находится на расстоянии +`sqrt(hA² + hB²)`. Для равных толщин это `1.414 × H > 1.25 × H`, хотя весь +треугольник лежит внутри пересечения двух настоящих полос. Безусловное +вычитание поэтому удаляет не mitre-spike, а сохранённый wall material. +Смешанные толщины дают тот же класс ошибки большей площади. + +Тесты #272 проверяли только связность пустоты, single retained/discarded probes +и синтетические cases. Code review прямо не сверял cases с приватными +экспортами и не делал ручной UI-check. Ни один gate не проверял отношение +`incident strip union ⊆ canonical masonry`, поэтому релизный escape был +возможен. + +## 4. Что человек увидит до и после + +**До:** на месте непрерывного T/X-стыка белый уголок либо крупная выемка; +толщина входящего отрезка визуально уменьшается, хотя editor overlay показывает +полную ось. + +**После:** каждый конечный входящий отрезок сохраняет заданную полную толщину +до node. Стык заполнен. Если room-ring действительно образовала внешний +mitre-spike вне всех физических strips, он по-прежнему срезан прямой фаской +#249 и не выходит за `R`. + +Optimize валидного плана может остаться no-op. Raw, Preview, Apply, reload, +Plan/View/Static/Iso и световая модель показывают одну форму. + +## 5. Scope + +### Входит + +- канонический containment-инвариант для положительных finite incident strips; +- ограничение bevel-cut только материалом, не принадлежащим ни одной incident + strip; +- rectilinear T/X и неортогональный 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; +- минимизированные fixtures из обоих новых backups, exact-input local smoke, + production-bundle browser smoke, semantic raster crops и mutation; +- документация, changelog и корректировка прежнего hole-only testing contract. + +### Не входит + +- изменение persisted rooms/walls/partitions/columns/openings/layout; +- новая эвристика Optimize, изменение wall thickness либо удаление валидного + micro-dogleg; +- отмена finite supports #271; +- возврат неограниченного mitre-spike или изменение коэффициента `R = 1.25H`; +- новый UI, настройка, schema/model version, backend либо i18n; +- принятие golden baseline без просмотра exact-input crops. + +## 6. Канонический геометрический контракт + +### 6.1 Incident strip + +Для каждого canonical multi-wall node и каждого `ray.support` определяется +конечная полоса: + +- ось начинается в `node.point`; +- продольный параметр `t ∈ [0, support.length]`; +- поперечное расстояние `≤ support.halfDepth`; +- учитывается только положительный, finite, не-open support; +- duplicate room owners схлопываются действующей детерминированной картой. + +`requiredStripUnion(node)` — boolean union этих полос в действующей node mask. +Он является физическим минимумом masonry до применения явных opening cuts. + +### 6.2 Containment + +До opening cuts должно выполняться: + +```text +requiredStripUnion(node) − roomGeom = ∅ +``` + +с scale-relative boolean tolerance, существенно меньшим физической/экранной +точности. После opening cuts допускается только площадь, принадлежащая +подтверждённому opening slot. Pairwise bevel, room-ring winding и paper repair +не имеют права удалять остальную часть incident strip. + +Containment проверяется площадью vector difference и устойчивыми interior +samples. Одной точки node, числа polygon holes либо connected-component +классификации пустоты недостаточно. + +### 6.3 Bounded exterior bevel + +Room contours могут содержать material за `R`, который не принадлежит ни одной +incident strip. Только такой material разрешено удалить bevel-cut: + +```text +effectiveCut = pairwiseCut − requiredStripUnion(node) +``` + +Либо технически эквивалентной операцией, если доказаны те же множества. +Existing excessive-wedge probe #249 остаётся пустым, каждая join-вершина +остаётся в bound, но прямоугольные T/X не получают выемку в overlap strips. + +### 6.4 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 + +Room opening cuts выполняются после strip-safe node reconstruction по +существующей association. Independent partitions/drafts/columns объединяются +в существующем порядке. Тест отличает разрешённый opening slot от bevel-loss +и не объявляет любой белый sample допустимым только из-за близкого opening. + +### 6.6 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. + +## 7. Совместимость, UX, security и performance + +- Persisted schema и model version не меняются. +- 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. Новый + глобальный `O(E²)` обход и пересчёт на HA/theme/hover tick запрещены. +- Local masks и число rays одного node ограничивают boolean work; benchmark + large-house не должен выйти за действующий prerelease budget. + +## 8. Acceptance criteria и доказательства + +### AC1. Оба новых реальных класса дают нулевую потерю strips + +Минимизированные анонимные fixtures содержат точные координаты/толщины минимум +узлов `(204.166667, 645.833333)` при `cell_cm: 5` и +`(-354.166667, 2087.5)` при `cell_cm: 1`, плюс по одному треугольному T из +каждого backup. Для каждого: + +- `difference(requiredStripUnion, roomGeom)` имеет нулевую площадь; +- interior sample inventory не находит пропусков; +- final geometry имеет тот же результат вне явных opening slots; +- node и все finite rays area-connected. + +**Доказательство:** fixture + unit в `test/wall-thickness.test.mjs`. + +### AC2. #249 остаётся ограниченным + +На fixture #249 old excessive-wedge area, не принадлежащая incident strips, +остаётся пустой; join не выходит за `R + epsilon`. Retained wedge #261 заполнен, +finite endpoints #271 не удлиняются, zero/open ray не материализуется. + +**Доказательство:** существующие probes плюс vector difference между pairwise +cut и required strips. + +### AC3. Exact-input lifecycle проходит на полных backups владельца + +Локальный некоммитящийся harness загружает `C:\Temp\4\1.json` и `2.json` и +проверяет четыре состояния: raw render, Optimize Preview, Apply + backend echo, +reload + повторный render. Для обоих файлов: + +- нарушений strip containment — `0` во всех degree-3+ nodes; +- Optimize не мутирует preview input; +- `2.json` вправе остаться `changed: false`, но render уже корректен; +- targeted crops не содержат прежних белых треугольников/крупного провала. + +**Доказательство:** локальный отчёт с SHA-256 входов, machine-readable inventory +и PNG-crops; приватные backups не попадают в commit/artifacts. + +### AC4. Browser и downstream consumers совпадают + +Production-bundle smoke проверяет настоящий SVG `isPointInFill()` в +Plan/View/kiosk/Static и vector geometry hidden Iso/light barriers. Paper и +clean-floor consumers сохраняют тот же boundary. Theme/HA tick переиспользуют +structural cache и не меняют path/fingerprint. + +**Доказательство:** targeted browser smoke на обеих minimized fixtures. + +### AC5. Visual gate видит именно выемку, а не только enclosed hole + +Golden/local crop semantic preflight проверяет required-strip containment до +pixel diff. Предрелизный baseline не может пройти, если пустота связана с +внешним фоном, но лежит внутри incident strip. Hole inventory #272 остаётся +дополнительной, а не достаточной проверкой. + +**Доказательство:** golden harness/matrix unit и просмотр Linux artifacts. + +### AC6. Мутант ловит release escape + +Исполняемый mutation возвращает безусловное вычитание полного pairwise cut из +ray-strip union. AC1 либо AC5 краснеют, даже если legacy +`enclosedHoles === 0`, retained probe и discarded probe остаются зелёными. + +**Доказательство:** отдельная запись `scripts/mutation-gate.mjs`, прогнанная в +реализации и code review. + +### AC7. Privacy и determinism + +В репозитории отсутствуют полные backups, layout и пользовательские названия. +Permutation rooms/walls, reversed interval endpoints, повторный расчёт, +canonical storage echo и оба grid scales дают одинаковый containment result и +не мутируют input. + +**Доказательство:** table-driven units и проверка Git diff/fixture contents. + +### AC8. Локальные гейты реализации + +- `npm run typecheck`; +- `npm test`; +- `npm run build` и bundle parity; +- `node scripts/check-docs.mjs`; +- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` и выбранные + targeted smokes; +- strip-containment mutation; +- targeted semantic golden capture. + +Полные smoke/golden/performance и Linux HA harness остаются prerelease gates. + +## 9. Ожидаемые файлы + +Product code: + +- `src/wall-thickness.ts`. + +Tests/evidence: + +- `test/wall-thickness.test.mjs` и минимизированные fixtures; +- `demo/smoke_multiwall_junction.mjs` либо новый exact-class smoke; +- `demo/golden/harness.mjs`, `demo/golden/matrix.mjs` и matrix unit; +- `scripts/mutation-gate.mjs`; +- локальный diagnostics harness может быть оформлен как reusable script, + принимающий внешний путь, но не должен содержать пользовательские данные. + +Документация: + +- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`; +- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`; +- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`. + +## 10. Release-артефакты + +Implementation-коммит имеет `Issue: #275`, `User-Visible: yes` и оба +changelog. Golden/docs baselines принимаются только после полного Linux +artifact и просмотра exact-class crops. Перед beta обязательны полные golden, +smoke, performance и exact-SHA Validate. + +## 11. Риски и меры + +| Риск | Мера | +|---|---| +| Возвратить spike, лишь перестав вычитать cuts | AC2 проверяет area вне strips и bound `R`. | +| Заполнить настоящий opening | AC1/AC3 различают pre-opening `roomGeom` и разрешённый slot. | +| Починить только SVG | AC4 проверяет paper/floor/Iso/light из общего source. | +| Снова принять похожую синтетику | AC1 требует точные minimized coordinates обоих backups, AC3 — полные файлы. | +| Golden пропустит внешний-connected notch | AC5 проверяет strip containment независимо от hole connectivity и pixel ratio. | +| Увеличить structural cost | Cached local node work и prerelease performance gate. | + +## 12. Rollback + +Откатываются product geometry, strip-containment units/smoke/mutation, visual +baselines и документация одним согласованным набором. Нельзя оставить новый +baseline при старом безусловном cut либо вернуть hole-only ожидание как +достаточное. Миграции данных нет. + +## 13. Принятые предположения + +1. Продуктовый ответ однозначен из репорта владельца: сохранённая положительная + полоса стены не может быть вырезана renderer-bevel. +2. Различающая формула `effectiveCut = pairwiseCut − requiredStripUnion` задаёт + множество, а не обязательную реализацию; эквивалентный алгоритм допустим. +3. Optimize для валидной осевой модели не обязан менять данные ради renderer + workaround. +4. Белые треугольники, потеря толщины и крупная выемка имеют одну доказанную + причину, поэтому ведутся одной задачей, а не тремя symptom issues. +5. Продуктовых вопросов нет; технические детали выбираются реализацией и + защищаются AC1–AC8.