From dd4ebb74012bab7e044046857b7ce4added6f0f6 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sun, 23 Aug 2026 19:39:58 +0300 Subject: [PATCH] docs(spec): require hole-free multi-wall joins Issue: #272 User-Visible: no --- docs/specs/272-no-multiwall-holes.md | 317 +++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 docs/specs/272-no-multiwall-holes.md diff --git a/docs/specs/272-no-multiwall-holes.md b/docs/specs/272-no-multiwall-holes.md new file mode 100644 index 00000000..fa29f786 --- /dev/null +++ b/docs/specs/272-no-multiwall-holes.md @@ -0,0 +1,317 @@ +# Issue #272 — без белых полостей в multi-wall стыках + +- Дата: 2026-08-23 +- Тип: bug · приоритет P1 +- Оценка: пользовательская ценность 10/10 · ценность для разработки 10/10 · + сложность 8/10 · риск 9/10 +- Issue: [#272](https://github.com/Matysh/houseplan-card/issues/272) +- Ветка: `issue/272-no-multiwall-holes` +- Статус ТЗ: готово к ревью + +Канонические документы: `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), +[#258](https://github.com/Matysh/houseplan-card/issues/258), +[#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). + +## 1. Сценарий и персона + +Администратор открывает два реальных плана после Optimize в свежей beta.5. В +обычных прямоугольных T/X-стыках между штрихованными полосами видны белые +треугольники. Они выглядят как отверстия в стене, сохраняются в View и на +разных масштабах и не соответствуют ни opening, ни room topology. + +Семья не может доверять главной визуальной поверхности. Это прямое нарушение +J1 и J6: физически непрерывный стык должен быть непрерывным у всех consumers, +а тест не может принимать белую дыру только потому, что она мала относительно +полного кадра. + +## 2. Подтверждённое воспроизведение + +Приватные экспорты `1.json` (`cell_cm: 5`, 12 degree-3+ nodes) и `2.json` +(`cell_cm: 1`, 14 nodes) прогнаны точной production geometry. Скриншоты +`1-1.png`, `1-3.png`, `2-2.png`, `2-3.png` показывают белые клинья на разных +сочетаниях равной и смешанной толщины. + +#261 исправила измеренный сектор около `(887.5, 550)` и добавила один +`retainedWedgeProbe`. Но она не проверила полную окрестность каждого node. + +#270 независимо измерила текущее состояние golden-сцены: + +- старый baseline содержит четыре запертые пустые компоненты; +- текущий код после #261 содержит две; +- pixel diff составляет меньше допуска сцены, поэтому golden проходит; +- добавляемый #270 inventory фиксирует `enclosedHoles: 2`, но сознательно не + решает, правильны ли они. + +Новый пользовательский репорт даёт продуктовый вердикт: видимых запертых +фоновых полостей в непрерывном стыке быть не должно. При этом внешний фон за +фаской и существующий discarded-wedge contract #249 должны остаться пустыми. + +Полные пользовательские экспорты не коммитятся. В репозиторий попадают только +минимизированные анонимные node fixtures для обоих grid scales. + +## 3. Подтверждённая зона причины + +`multiWallBevelTrianglesAt()` строит pairwise cut для последовательных rays, +если старое пересечение offset-lines дальше `R = 1.25 × H`. +`bevelMultiWallBody()`: + +1. вычитает full-origin pairwise triangles из исходного body; +2. независимо union-ит ray rectangles; +3. вычитает bounded excessive triangles; +4. добавляет tiny core; +5. клипует local result к envelope и подменяет часть исходной masonry в mask. + +`bevelMultiWallPaper()` применяет bounded triangles к paper отдельно. Эти +операции локально разумны по точечным probes #249/#261, но не гарантируют, что +границы нескольких pairwise cuts образуют один внешний bevel вместо +изолированного треугольного hole. Masonry и paper могут также оставить разные +локальные компоненты. + +Корневая зона доказана; точная boolean-перестройка остаётся техническим +решением реализации и должна выводиться из инварианта §6, а не подгоняться под +один пиксельный baseline. + +## 4. Что человек увидит до и после + +**До:** между реальными лучами T/X-стыка виден белый треугольник, иногда с +outline по краям, будто кладка разорвана. + +**После:** стык читается как единое сплошное тело. Если внешний угол ограничен +фаской #249, пустота лежит снаружи непрерывного контура и соединена с внешним +фоном; она не становится запертой дырой внутри masonry/paper. + +Новых UI, настроек и миграции нет. Исправление действует при чтении плана во +всех режимах. + +## 5. Scope + +### Входит + +- общий топологический инвариант для непрерывных degree-3+ wall nodes; +- согласованная локальная реконструкция body и paper без запертых фоновых + компонентов; +- T и X, равные/смешанные толщины, `cell_cm: 1/5`, разные winding/order; +- сохранение bounded exterior bevel и finite rays; +- room masonry, final masonry, paper, clean floors/fills/hover, Static/Iso и + light/sun barriers; +- unit geometry inventory, browser semantic inventory, mutation, targeted + smoke и golden crop; +- интеграция с #270 и осознанное принятие новых baselines; +- каноническая документация и оба changelog. + +### Не входит + +- изменение сохранённых rooms/walls/open spans/openings; +- Optimize data cleanup #273; +- удаление independent bodies; +- повышение общего pixel-diff порога либо попытка поймать дыру только порогом; +- отказ от `R = 1.25 × H` или возврат неограниченного mitre-spike; +- округление/скругление bevel, новый визуальный стиль стен; +- backend, schema/model version, i18n и UI. + +## 6. Геометрический контракт + +### 6.1 Required solid и внешний фон + +Для canonical node степени `3+` берутся конечные положительные physical rays. +Zero-depth, virtual/open rays material не добавляют. Required local solid — +связная область конечных ray strips и допустимого node join внутри canonical +paper/exterior envelope. + +Из required solid можно удалить excessive mitre area за `R`, только если +результат образует внешний bevel: удалённая область должна быть связана с +внешним фоном относительно локального physical contour. Cut не может оставить +закрытый пустой остров между полосами стен. + +Следствия: + +- node core и каждый incident positive ray area-connected; +- local masonry не содержит polygon hole, целиком окружённый required solid; +- straight bevel может иметь discarded-wedge probe #249 снаружи contour; +- отсутствие hole не означает заливку всего квадратного mask; +- область вне finite ray/support #271 не материализуется. + +### 6.2 Masonry и paper + +`roomGeom`, final `geom` и `paperGeom` имеют одну physical boundary около node. +Paper покрывает canonical masonry и room centre, но не маскирует masonry-hole +белой накладкой. Final independent bodies/opening cuts сохраняют действующий +порядок и не используются для сокрытия дефекта room-wall join. + +### 6.3 Семантическое определение hole + +Тест не равняет «любую пустую точку в окне» дыре. Для каждого fixture/node: + +1. задаётся bounded local window, пересекающее все incident strips за join; +2. классифицируются physical fill и пустота реальным geometry/SVG API; +3. пустые samples, связанные с границей окна или с объявленным exterior + sector, считаются внешним фоном; +4. оставшиеся связные компоненты внутри required-solid envelope — holes; +5. ожидаемое число holes для непрерывного T/X node равно нулю. + +Pure vector test дополнительно проверяет polygon rings/area, browser inventory +проверяет фактический path после рендера. Raster pixel threshold не заменяет ни +одну из этих проверок. + +### 6.4 Clean floor и physics + +Clean floor не присваивает площадь стены комнате, а light/sun barrier не имеет +прохода там, где masonry сплошная. Все consumers используют один canonical +node; отдельный white patch поверх SVG запрещён. + +### 6.5 Детерминизм и failure isolation + +Rooms/walls order, reversed endpoints, winding и duplicate shared owner не +меняют geometry/hole inventory. Ошибка одного optional node изолируется по +контракту #197; обязательный structural boolean failure остаётся fail-dark. + +## 7. Compatibility, UX, touch, security и performance + +- Config/layout/model version не меняются, вход не мутируется. +- Legacy/canonical wall keys #258 дают одинаковую topology. +- Новых controls, i18n, focus/keyboard/touch/ARIA нет. +- Новых HA данных, URL/HTML, permissions и security boundaries нет. +- Semantic inventory исполняется в tests/golden harness, а не на каждом + production frame. Product geometry остаётся в cached structural pass без + нового state-tick traversal и без глобального pairwise `O(E²)`. + +## 8. Acceptance criteria и доказательства + +### AC1. Все реальные классы node не имеют запертых holes + +Minimized fixtures из обоих экспортов содержат как минимум: + +- rectilinear T с тремя равными half-depth; +- T со смешанной толщиной; +- X/degree-4; +- короткий incident ray после/вместе с finite-ray #271; +- normalized `cell_cm: 5` и high-resolution `cell_cm: 1`. + +Для каждого node vector inventory даёт `0` enclosed holes в `roomGeom`, final +masonry и paper; join связан со всеми rays. + +**Доказательство:** `test/wall-thickness.test.mjs` и анонимная fixture. + +### AC2. #249 остаётся ограниченным внешним bevel + +Existing `discardedWedgeProbe` остаётся пустым; geometry не выходит дальше +`R + epsilon`; пустая область probe связана с внешним сектором, а не заперта +внутри стены. `MITRE_LIMIT`/`MULTI_WALL_JOIN_LIMIT` не меняются без отдельного +решения. + +### AC3. #261 и #271 не регрессируют + +`retainedWedgeProbe` #261 заполнен в masonry/paper и не становится clean floor. +Finite short ray не удлиняется ради закрытия hole. Zero-depth interval не +материализуется. Existing #197 failure-isolation остаётся зелёным. + +### AC4. Browser surfaces показывают один непрерывный узел + +Targeted production-bundle smoke проверяет hole inventory настоящим +`isPointInFill()` для Plan/View/kiosk и Static. Hidden Iso source и light/sun +barriers не имеют прохода/разрыва. Theme, hover и HA update не меняют +structural fingerprint. + +### AC5. Golden больше не может принять белый клин + +Для `multiwall-junction-bevel-view-dark` и новых minimized real scenes: + +- semantic inventory выполняется до pixel comparison; +- `enclosedHoles`/эквивалентный contract равен нулю для перечисленных nodes; +- добавление одного локального hole переводит сцену в error независимо от его + доли кадра; +- stale pre-fix baseline не принимается автоматически. + +Если #270 уже слита, её временное значение `2` заменяется product contract, а +не сохраняется ради зелёного теста. Если не слита, реализация переносит её +полезный inventory без дублирующих механизмов. + +### AC6. Мутант воспроизводит слепой класс + +Mutation возвращает текущую pairwise реконструкцию/один из holes. AC1 или AC5 +обязаны падать. Отдельно доказывается, что single retained/discarded probes +могут остаться зелёными, то есть новый inventory действительно сильнее. + +### AC7. Privacy и determinism + +Полные пользовательские экспорты и имена комнат не коммитятся. Permutations, +reversed intervals, повторный расчёт и оба coordinate scales дают одинаковые +semantic outcomes без мутации input. + +### 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; +- mutation для enclosed hole; +- targeted semantic golden verify. + +Полные smoke/golden/performance и Linux HA harness — prerelease gates. + +## 9. Ожидаемые файлы + +Product code: + +- `src/wall-thickness.ts`. + +Tests/evidence: + +- `test/wall-thickness.test.mjs`, minimized fixture; +- targeted multiwall smoke; +- `demo/golden/harness.mjs`, `matrix.mjs`, `run.mjs` и matrix unit; +- `scripts/mutation-gate.mjs` и registries; +- координация с веткой/реализацией #270 без параллельных inventory helpers. + +Документация: + +- `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: #272`, `User-Visible: yes` и оба changelog. +Golden baseline `multiwall-junction-bevel-view-dark` и новые scenes принимаются +только из полного Linux artifact после независимого просмотра. При изменении +visual fingerprint docs screenshots также переснимаются Linux workflow и +принимаются штатной reviewed-командой. Перед beta обязательны полный golden, +smoke, performance и exact-SHA Validate. + +## 11. Риски и меры + +| Риск | Мера | +|---|---| +| «Закрыть дыру» возвратом длинного spike | AC2 и exterior-connectivity contract. | +| Залить внешний фон вокруг T | §6.3 различает exterior-connected emptiness и hole. | +| Маскировать дефект только paper | AC1/AC4 проверяют masonry, paper, floor и barriers. | +| Golden снова пройдет по общему порогу | Semantic inventory до pixel diff и mutant. | +| Конфликт с #270/#271 | Один inventory implementation; рекомендуемый порядок merge: #271, затем #272, затем обновление #270 evidence. | + +## 12. Rollback + +Откатывается product geometry вместе с vector/browser inventory, mutation, +golden baselines и документацией. Нельзя оставить baseline нового сплошного +стыка при старом коде или временное `enclosedHoles: 2` после нового кода. +Миграции данных нет. + +## 13. Принятые технические предположения + +1. Правильный критерий — не «закрасить любой белый пиксель», а исключить + interior-connected hole при сохранении внешнего bounded bevel. +2. Конкретный boolean algorithm выбирается реализацией; тестовый topology + contract обязателен. +3. #270 используется как инфраструктурная основа, но её временное ожидаемое + число не является продуктовым решением. +4. #271 рекомендуется слить первой, чтобы hole repair работал на конечных rays + и не закреплял ложные длинные rectangles. +5. Продуктовых вопросов нет: владелец явно признал видимые белые треугольники + артефактами, а сохранение exterior clamp уже зафиксировано #249.